# Star Trek LCARS Dashboard

**URL:** <https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894>\
**Category:** Dashboard\
**Tags:** node-red-dashboard\
**Created:** [12 October 2026 06:40 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894 "2026-10-12T06:40:27Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![ECarter262001](https://avatars.discourse-cdn.com/v4/letter/e/ecccb3/32.png) [@ECarter262001](https://discourse.nodered.org/u/ECarter262001)\
**Post date:** [12 October 2026 06:40 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894/1 "2026-10-12T06:40:27Z")

</div>

Been working to build a Star Trek LCARS style Dashboard. I'm very happy with it so far but would love to get some feedback on things that could be done to improve it. All the sections are touch compatible and break down into the specific sections data when clicked. The "EPS Lines" turn off when no activity is happening and speed up and slow down depending on how much energy is being requested vs the maximum energy allowed.

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/0/9/096f81ffa06d21e00d6354d5b450c0c1fc7444ce.png)

---

<div class="post-metadata">

**Author:** ![gerrybartley](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/gerrybartley/32/67495_2.png) [@gerrybartley](https://discourse.nodered.org/u/gerrybartley)\
**Post date:** [12 October 2026 07:22 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894/2 "2026-10-12T07:22:19Z")

</div>

Very nice. Can you post the flow? Would like to see how you implemented the EPS line animation

---

<div class="post-metadata">

**Author:** ![rakgupta](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/rakgupta/32/42535_2.png) [@rakgupta](https://discourse.nodered.org/u/rakgupta)\
**Post date:** [12 October 2026 10:47 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894/3 "2026-10-12T10:47:26Z")

</div>

![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/1/3/1395a5d3d88c8868ac031221322fe892e4add89b.jpeg)  
I currently have something like this and would love to take a look at the flow for what you have done. It does look great 👏🏼👏🏼👏🏼

---

<div class="post-metadata">

**Author:** ![ECarter262001](https://avatars.discourse-cdn.com/v4/letter/e/ecccb3/32.png) [@ECarter262001](https://discourse.nodered.org/u/ECarter262001)\
**Post date:** [12 October 2026 14:58 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894/4 "2026-10-12T14:58:21Z")

</div>

> [@gerrybartley](#):
>
> Very nice. Can you post the flow? Would like to see how you implemented the EPS line animation

Thanks! Happy to share how the lines work. I'm still tidying the full flow, so I'm not posting it yet, but the animation is simple enough to explain here.

Each line is an SVG path drawn in a few layers on the same `d`: a wide translucent glow, a dark housing, a coloured dashed body, and a thin white dashed core. The dark gaps between the dashes are what make it look like a segmented conduit. The movement is pure CSS, sliding `stroke-dashoffset`, and `animation-direction: reverse` flips the direction. `vector-effect: non-scaling-stroke` keeps the dash sizes in screen pixels when the SVG is scaled.

Speed and the number of lit segments come from the live reading. Vue sets CSS variables on each line:

```js
plVars(w, full) {
  const f = Math.min(1, Math.abs(parseFloat(w) || 0) / full), fs = Math.max(0.06, f);
  const lit = 14, gap = 4 + lit * (1 / fs - 1), per = lit + gap, d0 = 1.8 - 1.45 * f;
  return { '--pl-lit': lit + 'px', '--pl-gap': gap.toFixed(1) + 'px', '--pl-per': per.toFixed(1), '--pl-dur': (d0 * per / 18).toFixed(2) + 's' };
}

```

```css
.pl-wave { stroke-dasharray: var(--pl-lit) var(--pl-gap); animation: pl-run var(--pl-dur) linear infinite; }
@keyframes pl-run { from { stroke-dashoffset: var(--pl-per); } to { stroke-dashoffset: 0; } }

```

`full` is the breaker rating, so a line at 13% load shows a few lone segments, and one near its limit is solid, fast and turns red. For lines that have to reach HTML elements, I measure the element positions with `getBoundingClientRect` and rebuild the paths on resize, since Dashboard 2's `updated()` didn't fire reliably for me.

---

<div class="post-metadata">

**Author:** ![ECarter262001](https://avatars.discourse-cdn.com/v4/letter/e/ecccb3/32.png) [@ECarter262001](https://discourse.nodered.org/u/ECarter262001)\
**Post date:** [12 October 2026 15:16 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894/5 "2026-10-12T15:16:00Z")

</div>

I like your layout also! It is similar to my classic layout but way more in depth.It won't let me paste a json but the basic flow is below.

> **Minimal flow (Menu \> Import \> Clipboard)**
>
> ```json
> [
> {
> "id": "eps0000000000001",
> "type": "tab",
> "label": "EPS line demo",
> "disabled": false,
> "info": ""
> },
> {
> "id": "eps0000000000002",
> "type": "comment",
> "z": "eps0000000000001",
> "name": "Minimal example: a function makes demo numbers, the template animates them. Choose a Dashboard group on the template node.",
> "info": "",
> "x": 380,
> "y": 60,
> "wires": []
> },
> {
> "id": "eps0000000000003",
> "type": "inject",
> "z": "eps0000000000001",
> "name": "every 3 s",
> "props": [
> {
> "p": "payload"
> }
> ],
> "repeat": "3",
> "crontab": "",
> "once": true,
> "onceDelay": "1",
> "topic": "",
> "payload": "",
> "payloadType": "date",
> "x": 120,
> "y": 140,
> "wires": [
> [
> "eps0000000000004"
> ]
> ]
> },
> {
> "id": "eps0000000000004",
> "type": "function",
> "z": "eps0000000000001",
> "name": "Demo power readings",
> "func": "// EXAMPLE DATA: replace this node with your real inputs (Victron MQTT, Modbus, etc.)\n// and send the same four numbers in msg.payload.\nconst t = Date.now() / 1000;\nconst sun = Math.max(0, Math.sin(t / 40)); // slow day/night cycle\nconst solar = Math.round(5200 * sun);\nconst load = Math.round(1800 + 900 * Math.sin(t / 13) + 400 * Math.sin(t / 5));\nlet battery = Math.max(-2500, Math.min(2500, solar - load)); // + charging, - discharging\nconst grid = Math.max(0, load - solar + Math.min(0, battery)); // import only what solar + battery can't cover\nmsg.payload = { solar, battery, grid, load };\nreturn msg;",
> "outputs": 1,
> "timeout": 0,
> "noerr": 0,
> "initialize": "",
> "finalize": "",
> "libs": [],
> "x": 340,
> "y": 140,
> "wires": [
> [
> "eps0000000000005"
> ]
> ]
> },
> {
> "id": "eps0000000000005",
> "type": "ui-template",
> "z": "eps0000000000001",
> "group": "",
> "page": "",
> "ui": "",
> "name": "Energy flow lines",
> "order": 1,
> "width": "6",
> "height": "0",
> "head": "",
> "format": "<template>\n <div class=\"eps-demo\">\n <svg viewBox=\"0 0 400 300\" class=\"eps-svg\" aria-label=\"Energy flow example\">\n <g v-for=\"l in lines\" :key=\"l.id\" class=\"pl\" :class=\"[l.cls, {on: l.on, reverse: l.rev}]\" :style=\"l.vars\">\n <path class=\"pl-glow\" :d=\"l.d\"/>\n <path class=\"pl-hous\" :d=\"l.d\"/>\n <path class=\"pl-wave\" :d=\"l.d\"/>\n <path class=\"pl-hot\" :d=\"l.d\"/>\n </g>\n <g v-for=\"n in nodes\" :key=\"n.id\">\n <circle :cx=\"n.x\" :cy=\"n.y\" r=\"30\" class=\"eps-node\" :style=\"{stroke: n.color}\"/>\n <text :x=\"n.x\" :y=\"n.y - 2\" text-anchor=\"middle\" class=\"eps-name\">{{ n.name }}</text>\n <text :x=\"n.x\" :y=\"n.y + 14\" text-anchor=\"middle\" class=\"eps-val\">{{ n.watts }} W</text>\n </g>\n <circle cx=\"200\" cy=\"150\" r=\"24\" class=\"eps-core\"/>\n <text x=\"200\" y=\"154\" text-anchor=\"middle\" class=\"eps-name\">CORE</text>\n </svg>\n </div>\n</template>\n\n<script>\n// Minimal example: four power readings drive four animated \"conduit\" lines.\n// Expected msg.payload: { solar: W, battery: W (+ charging / - discharging), grid: W (+ importing), load: W }\nexport default {\n data() {\n // Rated limit for each line, in watts. Edit these to match your system.\n return { limits: { solar: 6000, grid: 9600, load: 9600, battery: 5000 } }\n },\n computed: {\n p() { return (this.msg && this.msg.payload) || { solar: 0, battery: 0, grid: 0, load: 0 } },\n nodes() {\n const p = this.p, r = (v) => Math.round(Math.abs(Number(v) || 0))\n return [\n { id: 's', name: 'SOLAR', x: 60, y: 50, color: '#00ff66', watts: r(p.solar) },\n { id: 'g', name: 'GRID', x: 340, y: 50, color: '#4fa3ff', watts: r(p.grid) },\n { id: 'b', name: 'BATTERY', x: 60, y: 250, color: '#ffd700', watts: r(p.battery) },\n { id: 'h', name: 'HOUSE', x: 340, y: 250, color: '#ff9900', watts: r(p.load) }\n]\n },\n lines() {\n const p = this.p, L = this.limits, n = (v) => Number(v) || 0\n const bat = n(p.battery)\n return [\n this.line('solar', 'M90 66 L178 134', n(p.solar), L.solar, false),\n this.line('grid', 'M310 66 L222 134', n(p.grid), L.grid, false),\n this.line('house', 'M222 166 L310 234', n(p.load), L.load, false),\n // The battery path is drawn battery -> core; when charging, the line runs the other way\n this.line('battery', 'M90 234 L178 166', bat, L.battery, bat > 0)\n]\n }\n },\n methods: {\n line(cls, d, w, full, rev) {\n const f = Math.min(1, Math.abs(w) / full), fs = Math.max(0.06, f)\n const lit = 14, gap = 4 + lit * (1 / fs - 1), per = lit + gap, d0 = 1.8 - 1.45 * f\n return {\n id: cls, cls: 'pl-' + cls, d, rev, on: Math.abs(w) > 20,\n vars: { '--pl-lit': lit + 'px', '--pl-gap': gap.toFixed(1) + 'px', '--pl-per': per.toFixed(1), '--pl-dur': (d0 * per / 18).toFixed(2) + 's' }\n }\n }\n }\n}\n</script>\n\n<style>\n.eps-demo { background: #000; padding: 8px; border-radius: 8px; }\n.eps-svg { width: 100%; max-width: 560px; display: block; margin: 0 auto; }\n.eps-node { fill: #05070a; stroke-width: 3; }\n.eps-core { fill: #0a2a40; stroke: #4cc3ff; stroke-width: 3; }\n.eps-name { fill: #cfd8e3; font: 600 9px sans-serif; letter-spacing: .08em; }\n.eps-val { fill: #fff; font: 600 11px sans-serif; }\n\n/* The conduit lines: four stacked paths share one \"d\": glow, dark housing, coloured dashed body, white dashed core. */\n.pl path { fill: none; stroke-linecap: butt; vector-effect: non-scaling-stroke; opacity: 0; }\n.pl.on path { opacity: 1; }\n.pl-glow { stroke-width: 12px; } .pl.on .pl-glow { opacity: .22; }\n.pl-hous { stroke: #06070b; stroke-width: 9px; }\n.pl-wave { stroke-width: 6px; }\n.pl-hot { stroke: #fff; stroke-width: 2px; }\n/* Dash length and gap come from the live reading (set per line from the script): more load = denser and faster. */\n.pl-wave, .pl-hot { stroke-dasharray: var(--pl-lit, 14px) var(--pl-gap, 4px); animation: pl-run var(--pl-dur, 1s) linear infinite; }\n.pl.reverse .pl-wave, .pl.reverse .pl-hot { animation-direction: reverse; }\n.pl-solar .pl-glow, .pl-solar .pl-wave { stroke: #00ff66; }\n.pl-grid .pl-glow, .pl-grid .pl-wave { stroke: #4fa3ff; }\n.pl-battery .pl-glow, .pl-battery .pl-wave { stroke: #ffd700; }\n.pl-house .pl-glow, .pl-house .pl-wave { stroke: #ff9900; }\n@keyframes pl-run { from { stroke-dashoffset: var(--pl-per, 18); } to { stroke-dashoffset: 0; } }\n@media (prefers-reduced-motion: reduce) { .pl-wave, .pl-hot { animation: none; } }\n</style>\n",
> "storeOutMessages": true,
> "passthru": false,
> "resendOnRefresh": true,
> "templateScope": "local",
> "className": "",
> "x": 590,
> "y": 140,
> "wires": [
> []
> ]
> }
> ]
> 
> ```

---

<div class="post-metadata">

**Author:** ![gerrybartley](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/gerrybartley/32/67495_2.png) [@gerrybartley](https://discourse.nodered.org/u/gerrybartley)\
**Post date:** [12 October 2026 17:19 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894/6 "2026-10-12T17:19:01Z")

</div>

That's very nice indeed

---

<div class="post-metadata">

**Author:** ![rakgupta](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/rakgupta/32/42535_2.png) [@rakgupta](https://discourse.nodered.org/u/rakgupta)\
**Post date:** [14 October 2026 00:16 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894/7 "2026-10-14T00:16:54Z")

</div>

> [@ECarter262001](#):
>
> but the basic flow is below.

Just imported this and it is cool! I have to figure out how I can use it... my system updates every 5 minutes.

---

<div class="post-metadata">

**Author:** ![ECarter262001](https://avatars.discourse-cdn.com/v4/letter/e/ecccb3/32.png) [@ECarter262001](https://discourse.nodered.org/u/ECarter262001)\
**Post date:** [14 October 2026 01:36 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894/8 "2026-10-14T01:36:55Z")

</div>

I am currently updating every 5 seconds and should have taken a screenshot last night because i got .my first red alert to popup when i was nearly maxed on the grid overnight while charging my car. Had the charge amps set to high to get my batteries back to the 6am target of 54%. Did just finish the solar view and absolutely love it myself. Didn't have a great solar day today but it was something at least.

 ![Screenshot_20261013_203507_Chrome](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/8/8/88e67a52a790a376df934b6f6d1fda4c8ebb5ee8.jpeg)

---

<div class="post-metadata">

**Author:** ![ECarter262001](https://avatars.discourse-cdn.com/v4/letter/e/ecccb3/32.png) [@ECarter262001](https://discourse.nodered.org/u/ECarter262001)\
**Post date:** [14 October 2026 01:38 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894/9 "2026-10-14T01:38:18Z")

</div>

![Screenshot_20261013_203751_Chrome](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/c/7/c7146a8aafb8f2696b6e0df65882734da7940147.jpeg)

---

<div class="post-metadata">

**Author:** ![rakgupta](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/rakgupta/32/42535_2.png) [@rakgupta](https://discourse.nodered.org/u/rakgupta)\
**Post date:** [14 October 2026 14:30 UTC](https://discourse.nodered.org/t/star-trek-lcars-dashboard/101894/10 "2026-10-14T14:30:56Z")

</div>

How are you getting the string data? I have Tesla solar and Powerwalls and they have shut down the "vitals" local endpoint ☹
