From c4d9ed76167f5f4df8af65878d8e9c7fccdd9c80 Mon Sep 17 00:00:00 2001 From: Yohann Parisien Date: Fri, 6 Mar 2026 13:39:54 +0100 Subject: [PATCH] feat: the number of segments of the bargraph is now dynamic, new color palette --- src/led-bar-graph-element.stories.ts | 62 ++++++---- src/led-bar-graph-element.ts | 178 +++++++++++++++++++++------ 2 files changed, 182 insertions(+), 58 deletions(-) diff --git a/src/led-bar-graph-element.stories.ts b/src/led-bar-graph-element.stories.ts index 87535ed..f734a0a 100644 --- a/src/led-bar-graph-element.stories.ts +++ b/src/led-bar-graph-element.stories.ts @@ -1,33 +1,53 @@ +/// + +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; import './led-bar-graph-element'; -export default { +interface LedBarGraphArgs { + color: string; + offColor: string; + values: number[]; +} + +const meta: Meta = { title: 'Led Bar Graph', component: 'wokwi-led-bar-graph', - argTypes: { - color: { control: { type: 'color' } }, - values: 'string', + parameters: { + docs: { + description: { + component: 'A rezisable LED bar graph component with configurable colors and values', + }, + }, }, args: { - values: '[1, 0, 1, 0, 1, 0, 1, 0, 1, 0]', color: 'red', + offColor: '#444', + values: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + } satisfies LedBarGraphArgs, + argTypes: { + color: { + control: 'select', + options: ['red', 'lime', 'blue', 'yellow', 'BRG', 'RYG', 'GYR', 'BCYR', 'BGYR'], + description: 'The color of a lit segment.', + }, + offColor: { control: 'color', description: 'The color of an unlit segment.' }, + values: { + control: 'object', + description: + 'The values for the individual segments: 1 for a lit segment, and 0 for an unlit segment.', + }, }, }; -const Template = ({ color, values }) => - html``; - -export const Red = Template.bind({}); -Red.args = { color: 'red' }; +export default meta; +type Story = StoryObj; -export const Green = Template.bind({}); -Green.args = { color: 'lime' }; - -export const Off = Template.bind({}); -Off.args = { color: 'lime', values: '[]' }; - -export const SpecialGYR = Template.bind({}); -SpecialGYR.args = { color: 'GYR', values: '[1,1,1,1,1,1,1,1,1,1]' }; - -export const SpecialBCYR = Template.bind({}); -SpecialBCYR.args = { color: 'BCYR', values: '[1,1,1,1,1,1,1,1,1,1]' }; +export const Default: Story = { + render: (args) => + html``, +}; diff --git a/src/led-bar-graph-element.ts b/src/led-bar-graph-element.ts index 66be28f..c192018 100644 --- a/src/led-bar-graph-element.ts +++ b/src/led-bar-graph-element.ts @@ -3,7 +3,6 @@ import { customElement, property } from 'lit/decorators.js'; import { ElementPin } from './pin'; import { mmToPix } from './utils/units'; -const segments = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; const mm = mmToPix; const anodeX = 1.27 * mm; const cathodeX = 8.83 * mm; @@ -14,69 +13,174 @@ const cyan = '#6cf9dc'; const yellow = '#f1d73c'; const red = '#dc012d'; -const colorPalettes: Record = { - GYR: [green, green, green, green, green, yellow, yellow, yellow, red, red], - BCYR: [blue, cyan, cyan, cyan, cyan, yellow, yellow, yellow, red, red], +interface PaletteDef { + color: string; + percent: number; +} + +const paletteDefinitions: Record = { + BCYR: [ + { color: blue, percent: 10 }, + { color: cyan, percent: 40 }, + { color: yellow, percent: 30 }, + { color: red, percent: 20 }, + ], + BGYR: [ + { color: blue, percent: 10 }, + { color: green, percent: 40 }, + { color: yellow, percent: 30 }, + { color: red, percent: 20 }, + ], + BRG: [ + { color: blue, percent: 10 }, + { color: red, percent: 20 }, + { color: green, percent: 70 }, + ], + RYG: [ + { color: red, percent: 60 }, + { color: yellow, percent: 10 }, + { color: green, percent: 30 }, + ], + GYR: [ + { color: green, percent: 50 }, + { color: yellow, percent: 30 }, + { color: red, percent: 20 }, + ], + YR: [ + { color: yellow, percent: 50 }, + { color: red, percent: 50 }, + ], }; @customElement('wokwi-led-bar-graph') export class LedBarGraphElement extends LitElement { - /** The color of a lit segment. Either HTML color or the special values GYR / BCYR */ + /** The color of a lit segment. + * Either HTML color or the special values. + * Special values are: + * - "BCYR": Blue-Cyan-Yellow-Red palette + * - "BGYR": Blue-Green-Yellow-Red palette + * - "BRG": Blue-Red-Green palette + * - "RYG": Red-Yellow-Green palette + * - "GYR": Green-Yellow-Red palette + * - "YR": Yellow-Red palette + */ @property() color = 'red'; /** The color of an unlit segment */ @property() offColor = '#444'; - readonly pinInfo: ElementPin[] = [ - { name: 'A1', x: anodeX, y: 1.27 * mm, number: 1, description: 'Anode 1', signals: [] }, - { name: 'A2', x: anodeX, y: 3.81 * mm, number: 2, description: 'Anode 2', signals: [] }, - { name: 'A3', x: anodeX, y: 6.35 * mm, number: 3, description: 'Anode 3', signals: [] }, - { name: 'A4', x: anodeX, y: 8.89 * mm, number: 4, description: 'Anode 4', signals: [] }, - { name: 'A5', x: anodeX, y: 11.43 * mm, number: 5, description: 'Anode 5', signals: [] }, - { name: 'A6', x: anodeX, y: 13.97 * mm, number: 6, description: 'Anode 6', signals: [] }, - { name: 'A7', x: anodeX, y: 16.51 * mm, number: 7, description: 'Anode 7', signals: [] }, - { name: 'A8', x: anodeX, y: 19.05 * mm, number: 8, description: 'Anode 8', signals: [] }, - { name: 'A9', x: anodeX, y: 21.59 * mm, number: 9, description: 'Anode 9', signals: [] }, - { name: 'A10', x: anodeX, y: 24.13 * mm, number: 10, description: 'Anode 10', signals: [] }, - { name: 'C1', x: cathodeX, y: 1.27 * mm, number: 20, description: 'Cathode 1', signals: [] }, - { name: 'C2', x: cathodeX, y: 3.81 * mm, number: 19, description: 'Cathode 2', signals: [] }, - { name: 'C3', x: cathodeX, y: 6.35 * mm, number: 18, description: 'Cathode 3', signals: [] }, - { name: 'C4', x: cathodeX, y: 8.89 * mm, number: 17, description: 'Cathode 4', signals: [] }, - { name: 'C5', x: cathodeX, y: 11.43 * mm, number: 16, description: 'Cathode 5', signals: [] }, - { name: 'C6', x: cathodeX, y: 13.97 * mm, number: 15, description: 'Cathode 6', signals: [] }, - { name: 'C7', x: cathodeX, y: 16.51 * mm, number: 14, description: 'Cathode 7', signals: [] }, - { name: 'C8', x: cathodeX, y: 19.05 * mm, number: 13, description: 'Cathode 8', signals: [] }, - { name: 'C9', x: cathodeX, y: 21.59 * mm, number: 12, description: 'Cathode 9', signals: [] }, - { name: 'C10', x: cathodeX, y: 24.13 * mm, number: 11, description: 'Cathode 10', signals: [] }, - ]; + get pinInfo(): readonly ElementPin[] { + const { values } = this; + const pinSpacing = 2.54 * mm; + const pins: ElementPin[] = []; + for (let i = 0; i < values.length; i++) { + const y = 1.27 * mm + i * pinSpacing; + pins.push({ + name: `A${i + 1}`, + x: anodeX, + y, + number: i * 2 + 1, + description: `Anode ${i + 1}`, + signals: [], + }); + pins.push({ + name: `C${i + 1}`, + x: cathodeX, + y, + number: i * 2 + 2, + description: `Cathode ${i + 1}`, + signals: [], + }); + } + return pins; + } /** * The values for the individual segments: 1 for a lit segment, and 0 for * an unlit segment. */ - @property({ type: Array }) values: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + @property({ type: Array }) values: number[] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; + + update(changedProperties: Map) { + if (changedProperties.has('values')) { + const oldValues = changedProperties.get('values') as number[] | undefined; + if (oldValues && oldValues.length !== this.values.length) { + this.dispatchEvent(new CustomEvent('pininfo-change')); + } + } + super.update(changedProperties); + } + + private getPaletteColors(name: string, numLeds: number): string[] | null { + const def = paletteDefinitions[name]; + if (!def) return null; + + // If numLeds is less than the number of colors in the palette, we can't respect the percentages. In that case, we just return the colors in order. + if (numLeds < def.length) { + return Array.from({ length: numLeds }, (_, i) => def[i % def.length].color); + } + + // Compute the number of LEDs for each color based on the percentages + const counts = def.map((d) => Math.round((d.percent / 100) * numLeds)); + + // At least one LED per color + for (let i = 0; i < counts.length; i++) { + if (counts[i] < 1) counts[i] = 1; + } + + // Adjust the counts to match the total number of LEDs (due to rounding) + let currentSum = counts.reduce((a, b) => a + b, 0); + while (currentSum !== numLeds) { + if (currentSum > numLeds) { + // Remove from the one that has the most LEDs (and at least 2 to avoid removing a color completely) + const index = counts.findIndex((c) => c === Math.max(...counts) && c > 1); + counts[index]--; + currentSum--; + } else { + // Add to the one that has the most percentage in the palette definition + const index = def.findIndex((d) => d.percent === Math.max(...def.map((x) => x.percent))); + counts[index]++; + currentSum++; + } + } + + // Color array constructed based on the counts + const colors: string[] = []; + for (let i = 0; i < def.length; i++) { + for (let j = 0; j < counts[i]; j++) { + colors.push(def[i].color); + } + } + return colors; + } render() { const { values, color, offColor } = this; - const palette = colorPalettes[color]; + const numLeds = values.length; + const palette = this.getPaletteColors(color, numLeds); + const pinPatternHeight = 2.54; + const rectHeight = numLeds * pinPatternHeight; + const svgHeight = rectHeight + 0.1; + const bodyPath = `m1.4 0h8.75v${svgHeight}h-10.1v-${svgHeight - 1.3}z`; + return html` - - - ${segments.map( - (index) => - svg` + ${values.map( + (value, index) => + svg``, )}