Warcraft Logs
Decorative background for title

Report Components: Best Practices

Last updated: November 24, 2022

There are several practices you can employ to help develop high-quality Report Components. These become much more important if you are planning on building something that you will then share with other people.

Optimize Performance

You may find yourself occasionally running into a "Script used more than allowed CPU time" error. This indicates that your script is simply doing too much work and is hitting the safeguards that Warcraft Logs has put in place to ensure our servers don't get overloaded.

But don't worry! The limits are very generous, and usually you can refactor your code to be more performant. Here are a couple of tips for common pitfalls:

  1. Understand how large your arrays are! The JavaScript array methods like filter & map are really helpful but have a performance cost (they copy the entire array each time). They are typically okay to use when dealing with arrays of hundreds of elements but should be avoided for arrays of thousands or more elements (such as all damage events for a player).

    For larger array manipulation, consider using reduce (which still copies the array but lets you combine multiple filter & map operations) or a single for loop.

    Note that it's usually perfectly fine to use filter & map when dealing with small arrays (for things such as players, abilities, deaths etc).

  2. The helper methods are usually cached, so are more performant and should be used as much as possible. These are things like fight.eventsByCategoryAndDisposition, fight.combatantInfoEvents, fight.friendlyDeathEvents etc. These are faster and usually more convenient than their alternatives.

Include a Title

Adding a title to your component is optional, but may be helpful when revisiting a report or sharing the component with another user. The different types of Report Components implement titles slightly differently:

Enhanced Markdown

The typical way to add a title to an EnhancedMarkdown component is to include a markdown heading in the content prop, such as:

return {
  component: 'EnhancedMarkdown',
  props: {
    content: `
# Title of the Component

Content of the component.
`
  }
}

Table

For Table, you can add a title by adding a header group for your columns:

return {
  component: 'Table',
  props: {
    columns: {
      title: {
        header: '**Title of the Component**',
        columns: {
          columnA: {
            header: 'Column A'
          },
          columnB: {
            header: 'Column B'
          }
        }
      }
    },
    data: [{
      columnA: 'Cell A',
      columnB: 'Cell B'
    }]
  }
}

Chart

As Chart uses the Highcharts API for its props, we can specify title and subtitle like so:

return {
  component: 'Chart',
  props: {
    title: {
      text: 'Title of the Component'
    },
    subtitle: {
      text: 'Subtitle of the Component'
    }
  }
}

Add Styling, Icons, and Tooltips

The Warcraft Logs UI uses various styling, icons, and tooltips to ensure it is provided quick context to its analysis. Most users can identify a player's class or an ability's school by color, and being able to hover over items and abilities to see tooltips with their in-game description can save a lot of time compared to having to lookup the ability separately.

Luckily, Enhanced Markdown includes lots of helper components for building these bits of UI:

Enhanced Markdown can be used in both the EnhancedMarkdown and Table Report Components. Chart does not currently support Enhanced Markdown, but the relevant colors for actors/abilities can be found using:

  • Actors: styles.getColorForActorType(actor.subType)
  • Abilities: styles.getColorForAbilityType(ability.type)

You can read their full documentation.

Check out the examples in the previous articles to see how the UI can be polished with these techniques.

Guard Against Unintended Use

You might design a component to do analysis for a particular encounter or to require a single player to be selected. In these scenarios, it's useful to guard against unintended use, especially if you plan on sharing the component.

For example, to ensure that every fight selected is for a particular encounter (in this case, Halondrus):

getComponent = () => {
  const halondrusEncounterId = 2529;
  const isHalondrus = reportGroup.fights
    .every(fight => fight.encounterId === halondrusEncounterId);

  if (!isHalondrus) {
    return {
      component: 'EnhancedMarkdown',
      props: {
        content: `This component only works for <EncounterIcon id="${halondrusEncounterId}">Halondrus</EncounterIcon>.`
      }
    }
  }

  return 'Not yet implemented.';
}

Or to ensure that only a single fight is picked and only a single player is filtered to:

getComponent = () => {
  const onlyOneFightSelected = reportGroup.fights.length === 1;
  const onlyOneCombatantInfoEvent =
    reportGroup.fights[0].combatantInfoEvents.length === 1;

  if (!onlyOneFightSelected || !onlyOneCombatantInfoEvent) {
    return {
      component: 'EnhancedMarkdown',
      props: {
        content: 'Please select a single fight and player to check enchants for.'
      }
    }
  }

  return 'Not yet implemented';
}