├── .gitignore ├── rollup.config.js ├── src ├── index.js ├── bar.js ├── utils.js ├── bar-group.js └── bar-chart.js ├── package.json ├── LICENSE ├── examples ├── time-series │ ├── style.css │ └── index.html ├── several-series │ ├── style.css │ └── index.html ├── theme.css ├── simple │ ├── style.css │ └── index.html └── fancy-mountains │ ├── style.css │ └── index.html ├── bar-chart.min.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .idea 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | export default { 3 | input: 'src/index.js', 4 | plugins: [terser()], 5 | }; 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Bar } from './bar.js'; 2 | import { BarChart } from './bar-chart.js'; 3 | import { BarGroup } from './bar-group.js'; 4 | 5 | customElements.define('bpapa-bar', Bar); 6 | customElements.define('bpapa-bar-group', BarGroup); 7 | customElements.define('bpapa-bar-chart', BarChart); 8 | -------------------------------------------------------------------------------- /src/bar.js: -------------------------------------------------------------------------------- 1 | export class Bar extends HTMLElement { 2 | static get observedAttributes() { 3 | return ['size']; 4 | } 5 | 6 | get value() { 7 | return Number(this.getAttribute('value') ?? '0'); 8 | } 9 | 10 | attributeChangedCallback(name, oldValue, newValue) { 11 | if (oldValue !== newValue && name === 'size') { 12 | this.style.setProperty('--bar-size', `${newValue}%`); 13 | } 14 | } 15 | 16 | connectedCallback() { 17 | if (!this.hasAttribute('slot')) { 18 | this.setAttribute('slot', 'bar-area'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barbapapa", 3 | "version": "0.1.0", 4 | "description": "lightweight library to build bar charts, based on web components", 5 | "type": "module", 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": { 9 | "import": { 10 | "default": "./src/index.js" 11 | } 12 | }, 13 | "./cdn": { 14 | "default": "./bar-chart.min.js" 15 | } 16 | }, 17 | "scripts": { 18 | "dev": "vite", 19 | "build": "rollup -c rollup.config.js > bar-chart.min.js", 20 | "size": "rollup -c rollup.config.js | brotli | wc -c" 21 | }, 22 | "prettier": { 23 | "singleQuote": true 24 | }, 25 | "files": ["src", "bar-chart.min.js"], 26 | "keywords": [ 27 | "bar", 28 | "chart", 29 | "webcomponents", 30 | "lightweight", 31 | "bar-chart", 32 | "dataviz", 33 | "data-visualization" 34 | ], 35 | "author": "Laurent RENARD", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "@rollup/plugin-terser": "^0.4.4", 39 | "prettier": "^3.2.5", 40 | "rollup": "^4.10.0", 41 | "vite": "^5.1.1", 42 | "zora": "^5.2.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 RENARD Laurent 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 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const compose = (fns) => (arg) => fns.reduceRight((x, f) => f(x), arg); 2 | const createScale = 3 | ({ domainMin, domainMax }) => 4 | (value) => 5 | (value - domainMin) / (domainMax - domainMin); 6 | 7 | const greaterOrEqual = (min) => (value) => Math.max(min, value); 8 | 9 | const lowerOrEqual = (max) => (value) => Math.min(max, value); 10 | 11 | const asPercentage = (val) => Math.floor(val * 10000) / 100; 12 | 13 | export const createProjection = ({ domainMin, domainMax }) => 14 | compose([ 15 | asPercentage, 16 | lowerOrEqual(1), 17 | greaterOrEqual(0), 18 | createScale({ domainMin, domainMax }), 19 | ]); 20 | 21 | export const round = (val) => Math.floor(val * 10_000) / 10_000; 22 | 23 | export const createTemplate = (templateString) => { 24 | const element = document.createElement('template'); 25 | element.innerHTML = templateString; 26 | return element; 27 | }; 28 | 29 | export const pick = (prop) => (target) => target[prop]; 30 | 31 | export const is = 32 | (expectedLocalName) => 33 | ({ localName }) => 34 | localName === expectedLocalName; 35 | export const pickValue = pick('value'); 36 | -------------------------------------------------------------------------------- /examples/time-series/style.css: -------------------------------------------------------------------------------- 1 | ::part(linear-axis) { 2 | font-size: 0.8em; 3 | block-size: var(--axis-size); 4 | align-self: flex-end; 5 | grid-column: 1 / -1; 6 | display: flex; 7 | flex-direction: column-reverse; 8 | justify-content: space-between; 9 | } 10 | 11 | ::part(bar-area) { 12 | border-left: 1px solid lightgray; 13 | border-bottom: 1px solid lightgray; 14 | } 15 | 16 | .tick-box { 17 | block-size: 1px; 18 | background-image: linear-gradient(to right, transparent, lightgray, transparent); 19 | background-size: 25px 1px; 20 | inline-size: 100%; 21 | text-align: left; 22 | margin-inline-start: auto; 23 | 24 | > span { 25 | text-align: right; 26 | display: inline-block; 27 | width: var(--tick-width); 28 | } 29 | } 30 | bpapa-bar-chart { 31 | --linear-axis-inline-size: 3em; 32 | height: 400px; 33 | } 34 | 35 | [slot=category] { 36 | display: grid; 37 | grid-template-rows: auto auto; 38 | grid-gap: 2px; 39 | justify-content: center; 40 | align-content: flex-start; 41 | 42 | .tick { 43 | height: 5px; 44 | width: 1px; 45 | background: gray; 46 | } 47 | 48 | .tick.big { 49 | height: 10px; 50 | } 51 | } 52 | 53 | .slow { 54 | background: lightcoral; 55 | } 56 | 57 | -------------------------------------------------------------------------------- /examples/several-series/style.css: -------------------------------------------------------------------------------- 1 | bpapa-bar-group { 2 | &:not([stack]) { 3 | padding-inline: 10px; 4 | border-inline: 1px dashed lightgray; 5 | } 6 | 7 | bpapa-bar:nth-child(2) { 8 | background-color: lightcoral; 9 | } 10 | } 11 | 12 | [data-serie='1'] { 13 | --serie-color: #3861b2; 14 | } 15 | 16 | [data-serie='2'] { 17 | --serie-color: #d27070; 18 | } 19 | 20 | .serie-toggle { 21 | display: grid; 22 | grid-template-columns: 1em 1fr; 23 | align-items: center; 24 | gap: 0.25em; 25 | 26 | [type=checkbox] { 27 | --_ribbon-color: var(--serie-color); 28 | appearance: none; 29 | background: var(--_ribbon-color); 30 | height: 1em; 31 | border: 1px solid gray; 32 | 33 | &:not(:checked) { 34 | --_ribbon-color: transparent; 35 | } 36 | } 37 | } 38 | 39 | bpapa-bar { 40 | --_background-color: var(--serie-color); 41 | background: var(--_background-color); 42 | } 43 | 44 | .controls:has([data-serie='1']:not(:checked)) { 45 | & + bpapa-bar-chart { 46 | 47 | bpapa-bar[data-serie='1'] { 48 | display: none; 49 | } 50 | } 51 | } 52 | 53 | .controls:has([data-serie='2']:not(:checked)) { 54 | & + bpapa-bar-chart { 55 | 56 | bpapa-bar[data-serie='2'] { 57 | display: none; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/bar-group.js: -------------------------------------------------------------------------------- 1 | import { createTemplate, pickValue } from './utils.js'; 2 | 3 | const template = createTemplate(` 4 | 28 | 29 | `); 30 | 31 | export class BarGroup extends HTMLElement { 32 | #slot; 33 | get value() { 34 | const assignedBars = this.#slot.assignedElements(); 35 | return this.hasAttribute('stack') 36 | ? assignedBars.reduce((total, { value }) => total + value, 0) 37 | : Math.max(...assignedBars.map(pickValue)); 38 | } 39 | constructor() { 40 | super(); 41 | const shadowRoot = this.attachShadow({ mode: 'open' }); 42 | shadowRoot.appendChild(template.content.cloneNode(true)); 43 | this.#slot = shadowRoot.querySelector('slot[name=bar-area]'); 44 | } 45 | 46 | connectedCallback() { 47 | if (!this.hasAttribute('slot')) { 48 | this.setAttribute('slot', 'bar-area'); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/theme.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | * { 8 | margin: 0; 9 | } 10 | 11 | body { 12 | line-height: 1.4; 13 | margin: unset; 14 | --animation-duration: 0.3s; 15 | -webkit-font-smoothing: antialiased; 16 | } 17 | 18 | button, 19 | input, 20 | textarea, 21 | select { 22 | font: inherit; 23 | } 24 | 25 | p, h1, h2, h3, h4, h5, h6 { 26 | overflow-wrap: break-word; 27 | } 28 | 29 | img, 30 | picture, 31 | svg, 32 | canvas { 33 | display: block; 34 | max-inline-size: 100%; 35 | block-size: auto; 36 | } 37 | 38 | body { 39 | font-family: system-ui, Avenir, sans-serif; 40 | min-height: 100svh; 41 | display: grid; 42 | grid-template-rows: auto 1fr; 43 | } 44 | 45 | main { 46 | display: grid; 47 | grid-template-rows: auto 1fr; 48 | gap: 2.5em; 49 | padding: 1em; 50 | } 51 | 52 | .controls { 53 | display: flex; 54 | justify-content: space-between; 55 | } 56 | 57 | 58 | bpapa-bar-chart { 59 | 60 | padding: 2em 0; 61 | 62 | &:not([horizontal]) { 63 | .label { 64 | container: label / inline-size; 65 | white-space: nowrap; 66 | 67 | > span { 68 | transform-origin: 0 0; 69 | transition: transform var(--animation-duration); 70 | } 71 | } 72 | } 73 | 74 | &[horizontal] { 75 | bpapa-bar { 76 | container: horizontal-bar / size; 77 | span { 78 | writing-mode: horizontal-tb; 79 | } 80 | } 81 | } 82 | 83 | &::part(category-axis) { 84 | font-size: 0.75em; 85 | } 86 | } 87 | 88 | 89 | bpapa-bar { 90 | transition: block-size var(--animation-duration); 91 | color: white; 92 | display: flex; 93 | justify-content: center; 94 | align-items: baseline; 95 | container: bpapa-bar / size; 96 | 97 | span { 98 | padding: 0.2em 0.5em; 99 | font-size: 0.9em; 100 | transition: transform var(--animation-duration); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/simple/style.css: -------------------------------------------------------------------------------- 1 | ::part(linear-axis) { 2 | font-size: 0.8em; 3 | block-size: var(--axis-size); 4 | align-self: flex-end; 5 | grid-column: 1 / -1; 6 | display: none; 7 | } 8 | 9 | ::part(bar-area) { 10 | border-block-end: 1px solid lightgray; 11 | } 12 | 13 | .tick-box { 14 | block-size: 1px; 15 | background-image: linear-gradient(to right, transparent, lightgray, transparent); 16 | background-size: 25px 1px; 17 | inline-size: 100%; 18 | text-align: left; 19 | margin-inline-start: auto; 20 | 21 | > span { 22 | text-align: right; 23 | display: inline-block; 24 | width: var(--tick-width); 25 | } 26 | 27 | &:first-of-type > * { 28 | display: none; 29 | } 30 | } 31 | 32 | 33 | 34 | .tick-box:first-of-type { 35 | background: none; 36 | } 37 | 38 | bpapa-bar-chart[horizontal] { 39 | .tick-box { 40 | background-image: linear-gradient(to bottom, transparent, lightgray, transparent); 41 | background-size: 1px 25px; 42 | span { 43 | rotate: -45deg; 44 | translate: -0.5em 0.5em; 45 | } 46 | } 47 | } 48 | 49 | .controls:has(#axis-checkbox:checked) { 50 | & + bpapa-bar-chart::part(linear-axis) { 51 | display: flex; 52 | flex-direction: column-reverse; 53 | justify-content: space-between; 54 | } 55 | 56 | & + bpapa-bar-chart { 57 | --linear-axis-inline-size: 2em 58 | } 59 | 60 | & + bpapa-bar-chart::part(bar-area) { 61 | border-inline-start: 1px solid lightgray; 62 | } 63 | } 64 | 65 | @container bpapa-bar (width < 3em) or (height < 2em) { 66 | span { 67 | --_bubble-size: 75%; 68 | background: inherit; 69 | padding: 0.2em 0.5em 0.5em 0.5em !important; 70 | transform: translateY(-2.2em); 71 | clip-path: polygon(0 0%, 0 var(--_bubble-size), 50% 100%, 100% var(--_bubble-size), 100% 0%); 72 | } 73 | } 74 | 75 | @container horizontal-bar (width < 3em) or (height < 1em) { 76 | span { 77 | --_arrow-size: 1em; 78 | background: inherit; 79 | padding-inline-start: 1em !important; 80 | transform: translateX(calc(100% + 0.2em)); 81 | clip-path: polygon(0 50%, var(--_arrow-size) 0, 100% 0, 100% 100%, var(--_arrow-size) 100%); 82 | } 83 | } 84 | 85 | @container bpapa-bar (width < 1px) or (height < 1px) { 86 | span { 87 | display: none; 88 | } 89 | } 90 | 91 | @container horizontal-bar (width < 1px) { 92 | span { 93 | display: none; 94 | } 95 | } 96 | 97 | @container label (max-width: 5em) { 98 | span { 99 | transform: translateX(30%) rotateZ(45deg); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple chart example 6 | 7 | 8 | 9 | 10 | 11 |

Simple example

12 |
13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 | 21 |
22 |
23 | 24 |
25 | 26 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /examples/several-series/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bar groups chart example 6 | 7 | 8 | 9 | 10 | 11 |

Bar group example

12 |
13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 28 | 32 |
33 |
34 | 35 | 36 |
37 | 38 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /examples/fancy-mountains/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #2a3544; 3 | color: #d8ebfc; 4 | padding: 1em; 5 | } 6 | 7 | main { 8 | padding-inline: unset; 9 | } 10 | 11 | bpapa-bar-chart { 12 | padding: 0; 13 | position: relative; 14 | min-height: 500px; 15 | border-radius: 10px 10px 0 0; 16 | background-image: linear-gradient(to bottom, #e8f3ff, #91abc7); 17 | 18 | &::part(linear-axis) { 19 | font-size: 0.8em; 20 | grid-column: 1 / -1; 21 | height: 80%; 22 | align-self: flex-end; 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: space-between; 26 | } 27 | 28 | &::part(category-axis) { 29 | background: #375a80; 30 | } 31 | 32 | } 33 | 34 | [slot=category] { 35 | color: white; 36 | text-align: center; 37 | padding: 0.25em 0.5em; 38 | place-items: unset; 39 | } 40 | 41 | bpapa-bar { 42 | container: bar / inline-size; 43 | width: 90%; 44 | position: relative; 45 | background: transparent; 46 | align-items: center; 47 | flex-direction: column; 48 | justify-content: space-between; 49 | isolation: isolate; 50 | 51 | &::before { 52 | content: ''; 53 | background-image: 54 | linear-gradient(to bottom, white, white 10%, rgba(255, 255, 255, 0.7) 25%, transparent 50%), 55 | linear-gradient(to bottom, #849fb7, #2a2e34); 56 | background-blend-mode: luminosity; 57 | clip-path: polygon(0% 100%, 5% 72%, 9% 80%, 16% 45%, 22% 31%, 27% 41%, 33% 12%, 39% 0%, 56% 26%, 60% 19%, 71% 55%, 76% 46%, 100% 100%); 58 | z-index: -1; 59 | position: absolute; 60 | inset:0; 61 | } 62 | 63 | .value { 64 | font-size: 0.9em; 65 | background: 66 | linear-gradient(to bottom, rgba(203, 149, 95, 0.5), transparent, rgba(203, 149, 95, 0.5)), 67 | linear-gradient(to bottom, #b6915a, transparent, #b6915a), 68 | saddlebrown; 69 | background-size: 100% 33%, 100% 10%; 70 | border-radius: 3px; 71 | box-shadow: 1px 1px 3px 0 #3f2819; 72 | color: white; 73 | padding: 0.25em 0.75em; 74 | translate: 0 5em; 75 | opacity: 0; 76 | z-index: -2; 77 | margin-inline: auto; 78 | transition: all var(--animation-duration); 79 | } 80 | 81 | &:hover { 82 | .value { 83 | opacity: 1; 84 | translate: 0 -3em; 85 | } 86 | } 87 | } 88 | 89 | 90 | [slot=linear-axis] { 91 | --_tick-color: #94abc7; 92 | --_tick-size: 1em; 93 | block-size: 1px; 94 | background-image: linear-gradient(to right, var(--_tick-color), var(--_tick-color) var(--_tick-size), transparent var(--_tick-size), transparent calc(var(--_tick-size) * 2)); 95 | background-size: calc(var(--_tick-size) * 2) 1px; 96 | inline-size: 100%; 97 | color: #2a3544; 98 | text-align: left; 99 | margin-inline-start: auto; 100 | 101 | > * { 102 | text-align: right; 103 | width: var(--linear-axis-inline-size); 104 | white-space: nowrap; 105 | padding-inline: 0.4em; 106 | translate: 0 -2.5em; 107 | 108 | &::first-letter { 109 | font-size: 1.3em; 110 | letter-spacing: 1px; 111 | } 112 | } 113 | 114 | &:last-of-type { 115 | visibility: hidden; 116 | } 117 | } 118 | 119 | .location { 120 | margin-bottom: 1em; 121 | display: grid; 122 | grid-template-rows: auto auto; 123 | grid-gap: 3px; 124 | transition: var(--animation-duration); 125 | 126 | > :first-child { 127 | overflow: hidden; 128 | } 129 | 130 | > :last-child { 131 | margin: auto; 132 | } 133 | 134 | } 135 | 136 | @container bar (width < 5em) { 137 | .location { 138 | grid-template-rows: 0 auto; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/bar-chart.js: -------------------------------------------------------------------------------- 1 | import { 2 | compose, 3 | createProjection, 4 | createTemplate, 5 | is, 6 | pickValue, 7 | round, 8 | } from './utils.js'; 9 | 10 | // language=HTML 11 | const template = createTemplate(` 12 | 58 |
59 | 60 |
61 |
62 | 63 |
64 |
65 | 66 |
67 | 68 | `); 69 | 70 | export class BarChart extends HTMLElement { 71 | #barArea; 72 | static get observedAttributes() { 73 | return ['domain-min', 'domain-max', 'stack']; 74 | } 75 | 76 | get domainMin() { 77 | return this.hasAttribute('domain-min') 78 | ? Number(this.getAttribute('domain-min')) 79 | : Math.min(...this.#barArea.assignedElements().map(pickValue)); 80 | } 81 | 82 | get domainMax() { 83 | return this.hasAttribute('domain-max') 84 | ? Number(this.getAttribute('domain-max')) 85 | : Math.max(...this.#barArea.assignedElements().map(pickValue)); 86 | } 87 | 88 | constructor() { 89 | super(); 90 | const shadowRoot = this.attachShadow({ 91 | mode: 'open', 92 | }); 93 | shadowRoot.append(template.content.cloneNode(true)); 94 | this.#barArea = this.shadowRoot.querySelector('slot[name=bar-area]'); 95 | this.render = this.render.bind(this); 96 | this.#barArea.addEventListener('slotchange', this.render); 97 | } 98 | 99 | attributeChangedCallback() { 100 | this.render(); 101 | } 102 | 103 | render() { 104 | const barsLike = this.#barArea.assignedElements(); 105 | this.style.setProperty('--_bar-count', barsLike.length); 106 | 107 | const groups = barsLike.filter(is('bpapa-bar-group')); 108 | 109 | const bars = barsLike 110 | .flatMap((barLike) => [barLike, ...Array.from(barLike.children)]) 111 | .filter(is('bpapa-bar')); 112 | 113 | groups.forEach((bar) => 114 | bar.toggleAttribute('stack', this.hasAttribute('stack')), 115 | ); 116 | 117 | const project = (this.project = compose([ 118 | round, 119 | createProjection(this), 120 | ]).bind(this)); 121 | 122 | bars.forEach((bar) => { 123 | bar.setAttribute('size', project(bar.value)); 124 | }); 125 | 126 | this.dispatchEvent(new CustomEvent('rendered', { bubbles: true })); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /examples/time-series/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Time series 6 | 7 | 8 | 9 | 10 | 11 |

Time series example

12 |
13 |

Fake server response time

14 |

A live stream of response time. Whenever a data point is above the threshold of 450ms, we tag it as "slow" and paint it in redish color

15 | 16 |
17 | 18 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /bar-chart.min.js: -------------------------------------------------------------------------------- 1 | class e extends HTMLElement{static get observedAttributes(){return["size"]}get value(){return Number(this.getAttribute("value")??"0")}attributeChangedCallback(e,t,n){t!==n&&"size"===e&&this.style.setProperty("--bar-size",`${n}%`)}connectedCallback(){this.hasAttribute("slot")||this.setAttribute("slot","bar-area")}}const t=e=>t=>e.reduceRight(((e,t)=>t(e)),t),n=({domainMin:e,domainMax:t})=>n=>(n-e)/(t-e),a=e=>Math.floor(1e4*e)/100,i=({domainMin:e,domainMax:i})=>{return t([a,(s=1,e=>Math.min(s,e)),(r=0,e=>Math.max(r,e)),n({domainMin:e,domainMax:i})]);var r,s},r=e=>Math.floor(1e4*e)/1e4,s=e=>{const t=document.createElement("template");return t.innerHTML=e,t},o=e=>({localName:t})=>t===e,l=(c="value",e=>e[c]);var c;const d=s('\n \n
\n \n
\n
\n \n
\n
\n \n
\n \n');class m extends HTMLElement{#e;static get observedAttributes(){return["domain-min","domain-max","stack"]}get domainMin(){return this.hasAttribute("domain-min")?Number(this.getAttribute("domain-min")):Math.min(...this.#e.assignedElements().map(l))}get domainMax(){return this.hasAttribute("domain-max")?Number(this.getAttribute("domain-max")):Math.max(...this.#e.assignedElements().map(l))}constructor(){super();this.attachShadow({mode:"open"}).append(d.content.cloneNode(!0)),this.#e=this.shadowRoot.querySelector("slot[name=bar-area]"),this.render=this.render.bind(this),this.#e.addEventListener("slotchange",this.render)}attributeChangedCallback(){this.render()}render(){const e=this.#e.assignedElements();this.style.setProperty("--_bar-count",e.length);const n=e.filter(o("bpapa-bar-group")),a=e.flatMap((e=>[e,...Array.from(e.children)])).filter(o("bpapa-bar"));n.forEach((e=>e.toggleAttribute("stack",this.hasAttribute("stack"))));const s=this.project=t([r,i(this)]).bind(this);a.forEach((e=>{e.setAttribute("size",s(e.value))})),this.dispatchEvent(new CustomEvent("rendered",{bubbles:!0}))}}const b=s('\n\n\n');class u extends HTMLElement{#t;get value(){const e=this.#t.assignedElements();return this.hasAttribute("stack")?e.reduce(((e,{value:t})=>e+t),0):Math.max(...e.map(l))}constructor(){super();const e=this.attachShadow({mode:"open"});e.appendChild(b.content.cloneNode(!0)),this.#t=e.querySelector("slot[name=bar-area]")}connectedCallback(){this.hasAttribute("slot")||this.setAttribute("slot","bar-area")}}customElements.define("bpapa-bar",e),customElements.define("bpapa-bar-group",u),customElements.define("bpapa-bar-chart",m); 2 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # barbapapa 4 | 5 | Web components to build bar charts in a declarative way. It "costs" 1292 bytes minified and compressed. It uses actual DOM elements, which gives you a lot of customization possibilities with regular CSS. 6 | It supports out of the box horizontal/vertical bars, multiple series and stacked bars 7 | 8 | A Bar chart presents categorical data with rectangular (or not) bars whose size is proportional to the values they represent within a domain of definition. 9 | 10 | ## Usage 11 | 12 | you can install the custom elements locally on your machine using a package manager (the package name is **barbapapa**, or directly import the file from a cdn inside your HTML document. 13 | Once the custom elements are defined, it is as simple as adding the element tags in your HTML document. 14 | 15 | ```html 16 | 17 | 18 | 19 | 20 | 21 | label 1 22 | 23 | label 2 24 | 25 | label 3 26 | 27 | label 4 28 | 29 | 30 | 31 | 32 | ``` 33 | 34 | You can then use regular css to customize your graph if needed. 35 | 36 | You can play around with the [following codepen](https://codepen.io/lorenzofox3/pen/RwdvwZM) 37 | 38 | ### elements 39 | 40 | #### bpapa-bar-chart 41 | 42 | It is the root component. It has few (reactive) attributes: **domain-min**, **domain-max**, **stack** and **horizontal**. 43 | 44 | **domain-*** define the data range. If not provided, they take the minimum and maximum values of the bars. 45 | 46 | **stack**: if present, it will stack the bars when they are in a bar-group 47 | 48 | **horizontal**: if present, the bars will be horizontal 49 | 50 | It emits the **rendered** event whenever a rendering finishes. 51 | 52 | Any element with the slot name **category** will be associated to the new category. They follow the order of definition to be associated with the matching bar/bar-group: the first occurrence of a category will be associated with the first bar, etc 53 | 54 | #### bpapa-bar 55 | 56 | It defines a bar of your dataset and takes a single attribute: **value** which represents the domain value associated with the bar. 57 | 58 | #### bpapa-group 59 | 60 | Let you group bars together. It is useful if you want to have multiple series (several bars associated with a single category). If the parent bpapa-bar-chart has the **stack** attribute the bars will stack on top of each other 61 | 62 | ```html 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ``` 75 | 76 | ## examples 77 | 78 | There are a bunch of examples provided in the repository, so you can see how easy it is to customize you bar chart 79 | 80 | 81 | ### Simple example 82 | 83 | It provides an example of the canonical bar chart. You can toggle the linear axis and the orientation. It is made responsive thanks to basic css code involving query containers 84 | 85 | https://github.com/lorenzofox3/bar-chart/assets/2402022/343fd444-3a5b-4897-9f44-11fff85bf2a2 86 | 87 | ### multiple series example 88 | 89 | A basic multiple series example which lets you toggle the direction, the stacking mode and the visibility of the series 90 | 91 | https://github.com/lorenzofox3/bar-chart/assets/2402022/1bd291ea-39a6-494b-a3d2-e85d93707f23 92 | 93 | ### time series example 94 | 95 | It has a dynamic dataset which gets updated every second. Time series are technically not adpated to bar charts, but here you have an existing example 96 | 97 | https://github.com/lorenzofox3/bar-chart/assets/2402022/c867129b-564e-4c07-bf99-f077fc86b01f 98 | 99 | ### CSS fun 100 | 101 | It shows how far you can go with the customization. With canvas based implementation, you are limited to what the framework offers. Here you can use the whole power of css 102 | 103 | https://github.com/lorenzofox3/bar-chart/assets/2402022/4e23c43f-1b97-4bdc-be81-bc23fe52e55f 104 | 105 | -------------------------------------------------------------------------------- /examples/fancy-mountains/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | List of highest mountains on Earth 6 | 7 | 8 | 9 | 10 | 11 |

List of highest mountains on Earth (by continent)

12 |
13 | 14 |
Mount Kosciuszko
15 | 16 |
2228m
17 |
18 | Australia 19 | flag of Australia 22 |
23 |
24 |
Mont Blanc
25 | 26 |
4806m
27 |
28 | France 29 | flag of France 32 |
33 |
34 |
Kilimandjaro
35 | 36 |
5892m
37 |
38 | Tanzania 39 | flag of Tanzania 42 |
43 |
44 |
Denali
45 | 46 |
6190m
47 |
48 | U.S.A 49 | flag of United States 52 |
53 |
54 |
Mount Everest
55 | 56 |
8849m
57 |
58 | Nepal 59 | flag of Nepal 61 |
62 |
63 |
Aconcagua
64 | 65 |
6962m
66 |
67 | Argentina 68 | flag of Argentina 71 |
72 |
73 |
Elbrouz
74 | 75 |
5643m
76 |
77 | Russia 78 | flag of Russia 80 |
81 |
82 |
Puncak Jaya
83 |
4884m
84 | 85 |
4884m
86 |
87 | Indonesia 88 | flag of Indonesia 91 |
92 |
93 |
Vinson Massif
94 | 95 |
4892m
96 |
97 | Antarctica 98 | flag of Antarctica 101 |
102 |
103 |
104 |
8000m
105 |
106 |
107 |
6000m
108 |
109 |
110 |
4000m
111 |
112 |
113 |
2000m
114 |
115 |
116 |
0
117 |
118 |
119 |
120 | 121 | 122 | 123 | 124 | --------------------------------------------------------------------------------