├── .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 |
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 |
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 |
22 |
23 |
24 | Mont Blanc
25 |
26 | 4806m
27 |
28 |
France
29 |
32 |
33 |
34 | Kilimandjaro
35 |
36 | 5892m
37 |
38 |
Tanzania
39 |
42 |
43 |
44 | Denali
45 |
46 | 6190m
47 |
48 |
U.S.A
49 |
52 |
53 |
54 | Mount Everest
55 |
56 | 8849m
57 |
58 |
Nepal
59 |
61 |
62 |
63 | Aconcagua
64 |
65 | 6962m
66 |
67 |
Argentina
68 |
71 |
72 |
73 | Elbrouz
74 |
75 | 5643m
76 |
77 |
Russia
78 |
80 |
81 |
82 | Puncak Jaya
83 | 4884m
84 |
85 | 4884m
86 |
87 |
Indonesia
88 |
91 |
92 |
93 | Vinson Massif
94 |
95 | 4892m
96 |
97 |
Antarctica
98 |
101 |
102 |
103 |
106 |
109 |
112 |
115 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------