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`