search envelope-o feed check
Home Unanswered Active Tags New Question
user comment-o

How to do totals by column and conditional coloring of cells

Asked by Peter
22 days ago.

In my application each event has as weight of sorts (think of it as a percentage), and I want to show the totals-by-column. If the total exceeds 100% I want to color it red. I got quite far but I have some loose ends, and I worry I may have the wrong approach.

Here’s what it looks like (ignore the blue columns, that’s something else.)

What I’m trying:

  • I store the event weight in event.tags.weight

  • In various places like onEventMoved or onTimeRangeSelected I call a recomputeWeights() function

  • My recomputeWeights() function iterates through all columns. I didn’t find a direct way to iterate the columns, but I’m doing
    for (cell of rows.find(“1”).cells.all())
    where row 1 is just a representative row, and that works well enough

  • I use events.forRange(cell.start, cell.end) to find all the events in that column, and add up the weights from the tags

  • I store that in a simple weights object where the key is cell.start and the value is the weight

  • Then, in onBeforeTimeHeaderRender and onBeforeCellRender I can look up the weight by cell-start in my weights object

A few things are not yet working, or feel icky:

  • When you drag to create a new event, I call events.add(), and then recomputeWeights(), but the new event isn’t present yet (race condition) so it doesn’t update correctly

  • I haven’t found where to compute the weights on first open

  • I have to “prod” the Scheduler to repaint the headers. I’m doing:
    dp.update({

    timeHeaders: [{"groupBy":"Month"},{"groupBy":"Day","format":"d"},{"groupBy":"Day","format":"d"}],

    });
    Which isn’t changing the timeHeaders, just asking them to get redrawn.

Is there a better approach for any of this?

Comment posted by Peter
22 days ago.

Attaching a screenshot.

Answer posted by Dan Letecky [DayPilot]
21 days ago.

Sorry for the delay.

I would make a few changes:

  1. Instead of using a special time header row, use a frozen row.

  2. Mark this row with cellsAutoUpdated: true, that will re-render the content on every change (events loaded, event moved or created…).

  3. Use onBeforeCellRender to calculate and display the column total (there is no need to pre-calculate it).

The key elements could look like this:

A. When loading resources, prepend the frozen row with the totals

async loadResources() {
  const {data} = await DayPilot.Http.get("/resources/");
  const resources = [
    {name: "Totals", id: "totals", frozen: "top", cellsAutoUpdated: true},
    ...data
  ];
  dp.update({resources});
}

B. Calculate the totals:

const scheduler = new DayPilot.Scheduler("scheduler", {
  onBeforeCellRender: args => {
    if (args.cell.resource === "totals") {
      const events = scheduler.events.forRange(args.cell.start, args.cell.end);

      const total = events.reduce((sum, e) => {
        const weight = e.data?.tags?.weight ?? 0;
        return sum + (Number.isFinite(n) ? n : 0);
      }, 0);

      args.cell.properties.text = total;
    }
  },
  // ..
});
// ...

You can also take a look at the following tutorial which implements a simplified version of this logic (it just counts the events):

Let me know if it doesn’t work as expected.

Comment posted by Dan Letecky [DayPilot]
21 days ago.

And to color the cells, you can update the cache object (totals) when rendering the frozen row and reuse the value for the standard cells:

const totals = {};

const scheduler = new DayPilot.Scheduler("scheduler", {
  onBeforeCellRender: args => {
    if (args.cell.resource === "totals") {
      const events = scheduler.events.forRange(args.cell.start, args.cell.end);

      const total = events.reduce((sum, e) => {
        const weight = e.data?.tags?.weight ?? 0;
        return sum + (Number.isFinite(n) ? n : 0);
      }, 0);

      args.cell.properties.text = total;
      
      if (total > 100) {
        args.cell.properties.backColor = "#cc0000";
      }
      
      totals[args.cell.start.toString()] = total;
    }
    else {
      if (totals[args.cell.start.toString()] > 100) {
        args.cell.properties.backColor = "#ee0000";
      }
    }
  },
  // ..
});

Cells are always rendered from top to bottom. The frozen row will be updated on every change (cellsAutoUpdated). That ensures the cached values will be up-to-date when rendering standard cells.

Comment posted by Peter
21 days ago.

Thanks, I’ll definitely try this. Since my totals are effectively a column property, I was trying too hard to compute it per column, but obviously per-cell will be fast enough, which removes a chunk of my convolutions.

Comment posted by Peter
20 days ago.

OK, that went smoothly and it’s a lot cleaner. A few notes and issues:

  • Because moving an event into a new column alters the totals, that can affect the backColor of the standard cells, so those need to get repainted. Therefore, I made all the resources cellsAutoUpdated: true. (So that’s solved)

  • It turns out that a resource with frozen: “top” is rendered last, not first, so caching the totals during that resource’s onBeforeCellRender is too late. In my case I don’t have a lot of resources, so I just removed frozen, and then top-to-bottom rendering fixes things. (So that’s solved)

  • I had wanted to recolor groupBy:Day timeheader when the total was > 100%, but that doesn’t get repainted (since I removed my update(timeHeaders) hack). I can do without that, but let me know if there’s an easy solution or if you plan to add something like cellsAutoUpdated for header rows.

Thank you!

Comment posted by Dan Letecky [DayPilot]
20 days ago.

Thanks for the update.

At this moment, the time headers are only updated if you call update().

If you call it without parameters, it refreshes everything (including time headers and cells). It is designed to be fast (only the viewport is rendered) so for smaller grids it might be the easiest solution.

However, you would have to call it manually from all places where events can be changed (like you did before).

New Reply
This reply is
Attachments:
or drop files here
Your name (optional):