Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 41 additions & 21 deletions src/led-bar-graph-element.stories.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,53 @@
/// <reference types="storybook/test" />

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`<wokwi-led-bar-graph values=${values} color=${color}></wokwi-led-bar-graph>`;

export const Red = Template.bind({});
Red.args = { color: 'red' };
export default meta;
type Story = StoryObj<LedBarGraphArgs>;

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`<wokwi-led-bar-graph
.color=${args.color}
.offColor=${args.offColor}
.values=${args.values}
></wokwi-led-bar-graph>`,
};
178 changes: 141 additions & 37 deletions src/led-bar-graph-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,69 +13,174 @@ const cyan = '#6cf9dc';
const yellow = '#f1d73c';
const red = '#dc012d';

const colorPalettes: Record<string, string[]> = {
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<string, PaletteDef[]> = {
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<string, unknown>) {
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`
<svg
width="10.1mm"
height="25.5mm"
height="${svgHeight}mm"
version="1.1"
viewBox="0 0 10.1 25.5"
viewBox="0 0 10.1 ${svgHeight}"
xmlns="http://www.w3.org/2000/svg"
>
<pattern id="pin-pattern" height="2.54" width="10.1" patternUnits="userSpaceOnUse">
<circle cx="1.27" cy="1.27" r="0.5" fill="#aaa" />
<circle cx="8.83" cy="1.27" r="0.5" fill="#aaa" />
</pattern>
<path d="m1.4 0h8.75v25.5h-10.1v-24.2z" />
<rect width="10.1" height="25.4" fill="url(#pin-pattern)" />
${segments.map(
(index) =>
svg`<rect x="2.5" y="${0.4 + index * 2.54}" width="5" height="1.74" fill="${
values[index] ? (palette?.[index] ?? color) : offColor
<path d=${bodyPath} />
<rect width="10.1" height="${rectHeight}" fill="url(#pin-pattern)" />
${values.map(
(value, index) =>
svg`<rect x="2.5" y="${0.4 + index * pinPatternHeight}" width="5" height="1.74" fill="${
value ? (palette?.[index] ?? color) : offColor
}"/>`,
)}
</svg>
Expand Down