मुख्य सामग्री पर जाएँ
CrossFly 100% ओपन सोर्सGitHub पर फ़ॉलो करें
CrossFlyCrossFly
शुरू करें
सभी उदाहरण
ऐपचार्ट495 पंक्तियाँ · 2 फ़ाइलें

Pulse — a dashboard

One screen, read rather than operated: a headline number, a KPI grid and charts, all re-fed by the single control that changes the period.

यह क्या दिखाता है

Pulse — the dashboard shape: one screen, read rather than operated.

gradlew lwjgl3:run --args="classname dev.cf.app.dashboard.DashboardApp"

What makes a dashboard different from a listing

A listing is scanned for one row. A dashboard is read in a fixed order, and the order is the design: headline, then supporting figures, then the shape over time, then the breakdown. A reader who has to hunt for the number the screen exists to show is looking at a chart gallery, not a dashboard.

Three decisions carry most of the weight here.

1 · One period, one snapshot, one rebuild

The period chips are the only control. Changing one takes a fresh Metrics and rebuilds the body from it. Patching the pieces individually is faster and is how a dashboard ends up with a KPI from March next to a chart from April — the failure nobody notices in review and everybody notices in a meeting.

2 · Semantic colour is separate from the accent

Up and down are read at a glance and must not be the brand colour, or "on brand" and "doing well" become the same signal. They come from theme tokens all the same (Styles.primaryColor, secondary()), so the whole screen still follows the skin and dark mode.

3 · A number states its own units and its own comparison

"12,480" is not information. "12,480, up 12.4% on last week" is. Every KPI here carries a caption saying what it counts, and the headline carries its delta.

अपनी मशीन पर चलाएँ

हर उदाहरण एक साधारण Window है, इसलिए लॉन्चर को सिर्फ़ उसका नाम चाहिए।

gradlew lwjgl3:run --args="classname dev.cf.app.dashboard.DashboardApp"
इसे बदलना

Java पहले से संकलित होता है, इसलिए ब्राउज़र आपके बदलाव को खुद दोबारा नहीं बना सकता — यहाँ दिखा कोड प्रकाशित होने से पहले TeaVM द्वारा JavaScript में बदला गया था। फ़ाइल को किसी प्रोजेक्ट में कॉपी कीजिए और वह चारों प्लेटफ़ॉर्म पर बिना बदलाव चलती है; ऊपर दी गई कमांड ही पूरी तैयारी है।

यहीं चल रहा हैCrossFly 0.1.32
ऐप शुरू हो रहा है…

TeaVM ने इसे JavaScript में संकलित किया — यह वही कोड है, उसका वीडियो नहीं। · सभी उदाहरण

यह क्या इस्तेमाल करता है

इस उदाहरण में छुए गए फ़्रेमवर्क टाइप। जिनमें लिंक है उनका संदर्भ पृष्ठ मौजूद है।

CardCardGridChartViewChipColorUtilsComboChartComponentDebugDonutChartGapHorizontalLayoutLabelPalColorRadialProgressRelativeScreenStylesVerticalLayoutWidgetsWindow

सोर्स कोड

पूरा, बिना कटौती के। यह वही फ़ाइल है, उसका अंश नहीं।

कच्चा
dev/cf/app/dashboard/DashboardApp.java
package dev.cf.app.dashboard;

import dev.cf.Described;
import dev.cf.Tones;
import dev.cf.app.AppBar;
import dev.cf.app.AppTheme;

import dev.crossfly.ui.*;
import dev.crossfly.ui.chart.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.color.ColorUtils;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;

import static dev.crossfly.ui.Widgets.*;

/**
 * <b>Pulse</b> — the dashboard shape: one screen, read rather than operated.
 *
 * <pre>
 * gradlew lwjgl3:run --args="classname dev.cf.app.dashboard.DashboardApp"
 * </pre>
 *
 * <h2>What makes a dashboard different from a listing</h2>
 *
 * <p>A listing is scanned for one row. A dashboard is read in a fixed order, and the order is the
 * design: <b>headline, then supporting figures, then the shape over time, then the breakdown</b>.
 * A reader who has to hunt for the number the screen exists to show is looking at a chart gallery,
 * not a dashboard.</p>
 *
 * <p>Three decisions carry most of the weight here.</p>
 *
 * <h3>1 · One period, one snapshot, one rebuild</h3>
 *
 * <p>The period chips are the only control. Changing one takes a fresh {@link Metrics} and rebuilds
 * the body from it. Patching the pieces individually is faster and is how a dashboard ends up with
 * a KPI from March next to a chart from April — the failure nobody notices in review and everybody
 * notices in a meeting.</p>
 *
 * <h3>2 · Semantic colour is separate from the accent</h3>
 *
 * <p>Up and down are read at a glance and must not be the brand colour, or "on brand" and "doing
 * well" become the same signal. They come from theme tokens all the same
 * ({@code Styles.primaryColor}, {@code secondary()}), so the whole screen still follows the skin
 * and dark mode.</p>
 *
 * <h3>3 · A number states its own units and its own comparison</h3>
 *
 * <p>"12,480" is not information. "12,480, up 12.4% on last week" is. Every KPI here carries a
 * caption saying what it counts, and the headline carries its delta.</p>
 */
public class DashboardApp extends Window implements Described
{
   private Metrics.Period period = Metrics.Period.WEEK;
   private Metrics data;

   private Component body;
   private Chip[] chips;

   @Override
   public String title()
   {
      return "Pulse — a dashboard";
   }

   @Override
   public String note()
   {
      return "One screen, read rather than operated: a headline number, a KPI grid and charts,"
         + " all re-fed by the single control that changes the period.";
   }

   /** Hands the palette back to whatever was in force before this app opened. */
   @Override
   public void onRemove()
   {
      super.onRemove();
      AppTheme.leave();
   }

   @Override
   public void onAdd(boolean firstTime) throws Exception
   {
      if (!firstTime)
         return;
      // The brand FIRST: fullscreenColor below reads a token, and reading it before the palette
      // is declared bakes in the skin's neutral grey for the life of the window.
      AppTheme.enter();
      // The PAGE, not the surface. Cards, buttons and fields are all drawn on the surface, so a
      // page painted the same colour makes every one of their edges disappear. Read through
      // onRestyle so a light/dark flip re-derives it instead of keeping the old mode's value.
      fullscreenColor = AppTheme.pageColor();
      onRestyle(() -> fullscreenColor = AppTheme.pageColor());
      setLayout(new VerticalLayout(Position.START, Position.START));

      add(0f, new AppBar("Pulse", isRoot() ? null : this::hide));
      add(0f, periodRow());

      body = new Component();
      body.setLayout(new VerticalLayout(Position.START, Position.START));
      body.setScrollable(ScrollDirection.VERTICAL);
      body.getStyle()
         .ingap(Gap.ofFont(1f, .5f, 1f, 1f));
      body.testTag("dash.body");
      add(LAY_FILL, body);

      rebuild();
   }

   /** The one control. Chips rather than a dropdown: three options a thumb can reach. */
   private Component periodRow() throws Exception
   {
      Component row = new Component();
      row.setLayout(new HorizontalLayout(Position.START, Position.CENTER));
      row.getStyle()
         .ingap(Gap.ofFont(1f, .5f, 1f, .5f));

      Metrics.Period[] all = Metrics.Period.values();
      chips = new Chip[all.length];
      for (int i = 0; i < all.length; i++)
      {
         final Metrics.Period p = all[i];
         Chip c = new Chip(p.label);
         c.testTag("dash.period." + p.name()
            .toLowerCase());
         c.setSelected(p == period);
         c.onAction(e ->
         {
            period = p;
            for (Chip other : chips)
               other.setSelected(false);
            c.setSelected(true);
            rebuild();
            Screen.repaint();
         });
         c.getStyle()
            .outgap(Gap.ofFont(0, 0, .4f, 0));
         chips[i] = c;
         row.add(LAY_AUTO, c);
      }
      return row;
   }

   /** Takes a snapshot and builds the whole body from it. Never partially. */
   private void rebuild()
   {
      try
      {
         data = Metrics.of(period);
         body.removeAll();
         // removeAll() drops the layout too — put it back before adding anything.
         body.setLayout(new VerticalLayout(Position.START, Position.START));

         body.add(LAY_AUTO, outgap(hero(), 0, 0, 0, 1f));
         body.add(LAY_AUTO, outgap(kpis(), 0, 0, 0, 1.2f));
         body.add(LAY_AUTO, outgap(sectionTitle("Revenue over time"), 0, 0, 0, .5f));
         body.add(LAY_AUTO, outgap(trend(), 0, 0, 0, 1.2f));
         body.add(LAY_AUTO, outgap(sectionTitle("Where it came from"), 0, 0, 0, .5f));
         body.add(LAY_AUTO, channels());

         body.setValidLayout(false);
      }
      catch (Exception e)
      {
         dev.crossfly.util.Debug.handle(e);
      }
   }

   /**
    * The hero: the one number the screen exists to show, its delta, and nothing else.
    *
    * <p>Resisting the fourth item here is most of the work. A hero with four figures in it has no
    * hero, and the reader has to decide which one matters — which is the decision the dashboard was
    * supposed to have made for them.</p>
    */
   private Component hero() throws Exception
   {
      Card c = new Card();
      c.setLayout(new VerticalLayout(Position.START, Position.START));
      c.testTag("dash.hero");

      c.add(LAY_AUTO, label("REVENUE · " + period.label.toUpperCase(), 1f, true,
         Styles.captionColor, Position.START));

      Label big = label(data.revenue(), 3f, true, Styles.textColor, Position.START);
      big.testTag("dash.revenue");
      c.add(LAY_AUTO, outgap(big, 0, .2f, 0, .3f));

      Component deltaRow = new Component();
      deltaRow.setLayout(new HorizontalLayout(Position.START, Position.CENTER));
      PalColor tone = data.deltaIsGood() ? Styles.primaryColor : secondary();
      deltaRow.add(LAY_AUTO, matglyph(data.deltaIsGood() ? 0xE5D8 : 0xE5DB, tone, 1.2f));
      Label d = label(data.delta() + " on the previous period", 1f, true, tone, Position.START);
      d.testTag("dash.delta");
      deltaRow.add(LAY_AUTO, outgap(d, .3f, 0, 0, 0));
      c.add(LAY_AUTO, deltaRow);

      return c;
   }

   /**
    * The supporting figures.
    *
    * <p>A {@link CardGrid} rather than a fixed row: on a phone these stack, on a tablet they sit
    * side by side, and neither case needed a width check or an orientation listener. Reflow is the
    * framework's job.</p>
    */
   private Component kpis() throws Exception
   {
      CardGrid g = new CardGrid().columns(2, 4)
         .cardWidth(11f)
         .gap(.6f);
      g.testTag("dash.kpis");
      g.card(kpi("Orders", "" + data.orders, "closed in the period", 0xE8CC));
      g.card(kpi("Customers", "" + data.customers, "who bought at least once", 0xE7EF));
      g.card(kpi("Average order", Metrics.money(data.orders == 0 ? 0
         : data.revenueCents / data.orders), "revenue ÷ orders", 0xE263));
      g.card(gauge());
      return g;
   }

   private Component kpi(String title, String value, String caption, int glyph) throws Exception
   {
      Card c = new Card();
      c.setLayout(new VerticalLayout(Position.START, Position.START));
      c.testTag("dash.kpi." + title.toLowerCase()
         .replace(' ', '.'));
      c.add(LAY_AUTO, matglyph(glyph, Styles.primaryColor, 1.4f));
      c.add(LAY_AUTO, outgap(label(value, 2f, true, Styles.textColor, Position.START), 0, .3f, 0,
         .1f));
      c.add(LAY_AUTO, label(title, 1f, true, Styles.textColor, Position.START));
      // The caption is what turns a number into a fact. It is the first thing cut for space and the
      // first thing missed in a meeting.
      c.add(LAY_AUTO, label(caption, 1f, false, Styles.captionColor, Position.START));
      return c;
   }

   /** The one KPI that is a ratio, so it gets the shape a ratio deserves. */
   private Component gauge() throws Exception
   {
      Card c = new Card();
      c.setLayout(new VerticalLayout(Position.CENTER, Position.CENTER));
      c.testTag("dash.kpi.conversion");
      RadialProgress r = new RadialProgress().progress(data.conversion)
         .text(data.conversionText())
         .caption("converted");
      c.add(LAY_AUTO, r);
      c.add(LAY_AUTO, outgap(label("Conversion", 1f, true, Styles.textColor, Position.CENTER), 0,
         .4f, 0, 0));
      return c;
   }

   /**
    * Revenue and orders on one chart.
    *
    * <p>Two series that share a period belong on one chart; two charts side by side make the reader
    * do the alignment themselves and they will do it wrong. The categories come from the same
    * snapshot as the values, which is what keeps the axis honest when the period changes.</p>
    */
   private Component trend() throws Exception
   {
      Card c = new Card();
      c.setLayout(new VerticalLayout(Position.START, Position.START));

      ComboChart chart = new ComboChart();
      chart.categories(data.labels);
      // The colour is passed explicitly. autoThemeColors below themes the axes, the grid and the
      // plot background, but NOT the series: a series left at colour 0 falls back to
      // ChartView.DEFAULT_COLORS, whose first entry is a dark grey — so the largest thing on the
      // screen ends up being the one thing that ignores the brand.
      chart.addSeries(new ChartView.Series("Revenue", data.revenueSeries,
         Styles.primaryColor.color()).bar());
      // The second series is the SAME hue, shaded towards the text colour rather than a different
      // colour entirely: the two are measuring the same thing over the same period, so they should
      // look related. Shading towards the text and not towards black is what keeps it working in
      // dark mode, where "darker" is the wrong direction.
      // A ROLE rather than the brand dimmed towards the text colour: two measures on one chart
      // have to be told apart without consulting the legend, and the dimmed brand read as grey.
      chart.addSeries(new ChartView.Series("Orders", data.ordersSeries, Tones.highlight().color())
            .line());
      chart.showLegend = true;
      chart.autoThemeColors(true);
      chart.getStyle()
         .autoHeight(Relative.ofFont(14f));
      chart.testTag("dash.trend");
      c.add(LAY_AUTO, chart);
      return c;
   }

   /** The breakdown. A donut, because the question is "what share", not "how much". */
   private Component channels() throws Exception
   {
      Card c = new Card();
      c.setLayout(new VerticalLayout(Position.START, Position.START));

      DonutChart d = new DonutChart();
      // One hue, four steps, darkest first — a share-of-total chart is asking "how much of it",
      // and four unrelated colours make the reader learn a legend to answer that. The ramp is
      // derived from the brand, so it follows a change of BRAND and it follows dark mode.
      int base = Styles.primaryColor.color(), ground = Styles.backColor.color();
      for (int i = 0; i < Metrics.CHANNEL.length; i++)
         d.slice(Metrics.CHANNEL[i], data.channelValues[i],
            ColorUtils.lerpColor(base, ground, i * .21f));
      d.hole(.62f);
      d.autoThemeColors(true);
      d.getStyle()
         .autoHeight(Relative.ofFont(14f));
      d.testTag("dash.channels");
      c.add(LAY_AUTO, d);
      return c;
   }

   private Component sectionTitle(String t) throws Exception
   {
      return label(t, 1.2f, true, Styles.textColor, Position.START);
   }
}
एआई सहायक के लिए

इस पृष्ठ के मशीन-पठनीय रूप, ताकि मॉडल API का अनुमान लगाने के बजाय उदाहरण पढ़ सके।

यह उदाहरण, JSON मेंयह उदाहरण, Markdown में
पिछलाDepot — offline-first CRUDअगलाFieldwork — capturing from the device