Skip to main content

Background

Measures are the fundamental building block of the grid builder. More specifically a measure is a datapoint or set of datapoints that relates to a serial number either at the process entry level or component instance level:
  • Process Entry Level: data related to a serial number via ****a process entry
  • Component Instance Level: data that’s inherently tied to the serial number via the unique_identifiers table
Measures can be any of the following types (note the difference between component instance level measures and process entry level measures)
export enum MeasureType {
  // Component Instance Level Measures
  Identifier = "IDENTIFIER",
  Status = "STATUS",
  LinkStatus = "LINK_STATUS",
  PartNumber = "PART_NUMBER",
  WorkOrder = "WORK_ORDER",
  WorkOrderPhase = "WORK_ORDER_PHASE",
  // Process Entry Level Measures
  Station = "STATION",
  Operator = "OPERATOR",
  CycleTime = "CYCLE_TIME",
  Timestamp = "TIMESTAMP",
  ProcessResult = "PROCESS_RESULT",
  ParametricQualitative = "PARAMETRIC_QUALITATIVE",
  ParametricQuantitative = "PARAMETRIC_QUANTITATIVE",
  Checkbox = "CHECKBOX",
  PassFail = "PASSFAIL",
  Image = "IMAGE",
  File = "FILE",
  Link = "LINK",
  Datetime = "DATETIME",
}

Measures Context

To utilize measures anywhere in the app you can wrap components in a <MeasuresProvider>
<MeasuresProvider componentId={selectedComponentId}>
  {/* child components go here */}
</MeasuresProvider>
In doing so you’ll be able to invoke the useMeasures hook to access the context

Identifying Measures

Since measures currently aren’t stored in any table they don’t have a primary key. Instead we identify measures either through a MeasureKey object or a hash of that object:
export interface MeasureKey {
  type: MeasureType;
  component_id: string; // snake_case used here since MeasureKeys may sometimes be stored in the database
  process_id?: string;
  dataset_id?: string;
  aggregation_operator?: MeasureAggregationOperator;
  formatting?: MeasureKeyFormatting;
}

export const hashMeasureKey = (measureKey: MeasureKey): string => {
  return `${measureKey.type}-${measureKey.component_id}-${measureKey.process_id}-${measureKey.dataset_id}`;
};

Loading Measures

Interface

The handleAddSelectedKey method from useMeasures adds a new measure key to the list of selected measure keys and triggers loading in the data. the <MeasureKeySelector> component can also be used to tho this and comes with a pre-built UI
const { handleAddSelectedKey } = useMeasures();

<MeasureKeySelector onSelect={handleAddSelectedKey} />

Under the Hood

UniqueIdentifierWithMeasures

When a new measure key gets added to the selectedMeasureKeys list, this triggers a fetches which load data into uniqueIdentifiersWithMeasures, the list of component instances of the selected component, which is of type UniqueIdentifierWithMeasures
export interface UniqueIdentifierWithMeasures extends UniqueIdentifier {
  genealogy: GenealogyByLinkedComponent;
  measures: {
    key: MeasureKey;
    valuesByIdentifier: {
      [uniqueIdentifierId: string]: MeasureValuesWithAggregation;
    };
  }[];
}

export interface GenealogyByLinkedComponent {
  uniqueIdentifiers: UniqueIdentifier[];
  uniqueIdentifierLinks: UniqueIdentifierLink[];
}
Let’s pause and fully understand the type definitions above. Note the following:
  • When the component ID is provided to the measures provider, it loads the component instances of that component as well as a genealogy object containing component instances and links (basically the nodes and edges in the tree). This information exists before any measure keys are selected
  • After a measure key is added, the measures provider fetches all the data for that measure and then inserts the data into the measures arrays in uniqueIdentifiersWithMeasures. Note that these measures arrays are build of objects containing the measure key and valuesByIdentifier - a map of uniqueIdentifierId to MeasureValuesWithAggregation. The reason for this is any given measure might apply to one or more members or the root component instance’s genealogy. For example a Drone might have 4 motors. If we want to load motor Max RPM onto the done, we’d be loading a single measure onto 4 members of the drone. In this case each drone’s valuesByIdentifier object for the Max RPM measure key would have 4 key value pairs:

MeasureValuesWithAggregation

As you may have noted, valuesByIdentifier is not as simple as mapping a UUID to a string / number. Here we’re mapping a UUID to yet another object of type MeasureValuesWithAggregation. There are two reasons for needing this additional complexity:
  1. A particular measure might have several retests even for a particular component instance. In the case of the drone motors, the process measuring Max RPM may have been run multiple times.
  2. The data being loaded is often more complex than just a string or a number, it often has additional metadata like pass/fail, USL, LSL, unit, a linked UUID, etc.
Below is the type definition for MeasureValuesWithAggregation:
export interface MeasureValuesWithAggregation {
  values: MeasureValue[];
  aggregation: MeasureValueSummary;
}

export interface MeasureValueSummary {
  value: string;
  formattedValue: string;
  tag: string | null;
  fileId?: string;
  bucketName?: string;
  href?: string;
}

// Note: the MeasureValue interface extends MeasureValueBase and has a bunch
// of different variants for each type of measure key
interface MeasureValueBase {
  uniqueIdentifierId: string;
  timestamp: TimestamptzString;
  data: any; // is is specified explicity in the extended versions of this interface
  processEntry?: ProcessEntry;
}
The aggregated measure value is calculated based on which aggregation operator is set in the MeasureKey. Aggregations can be one of the following:
export enum MeasureAggregationOperator {
  Latest = "LATEST",
  First = "FIRST",
  Count = "COUNT",
  Sum = "SUM",
  Average = "AVERAGE",
  Min = "MIN",
  Max = "MAX",
}

Filtering and Sorting Measures

The measures system provides robust filtering and sorting capabilities to help users analyze data effectively.

Filtering

Filtering is implemented using the filterBy array in the measures context. Each filter condition is represented by a MeasureFilterCondition object:
export interface MeasureFilterCondition {
  key: MeasureKey;
  operator: FilterConditionOperator;
  value: string;
}
Filters can be applied using the setFilterBy function from the useMeasures hook. The testFilter function in filters.ts evaluates these conditions against the measure values.

Sorting

Sorting is handled through the sortBy array in the measures context. Each sort condition is defined by a MeasureSortCondition:
export interface MeasureSortCondition {
  key: MeasureKey;
  direction: "asc" | "desc";
}
The setSortBy function from useMeasures can be used to update the sort order.

Displaying Measures

Grid View

The MeasuresGrid component provides a tabular view of measures data. It utilizes AG Grid for efficient rendering and supports features like:
  • Custom cell rendering based on measure type
  • Handling of blank data with explanations
  • Support for retests and aggregations

Graph View

For visualizing measure data, the system includes MeasuresGraphScatter and MeasuresGraphLine components. These leverage charting libraries to display data trends and relationships between different measures.

Advanced Features

Time Operators

Time operators allow for temporal analysis of measures. They can be set using the MeasureTimeOperator interface:
export interface MeasureTimeOperator {
  since?: MeasureKey | Date;
  until?: MeasureKey | Date;
}
The MeasureKeyTimeOperatorModal component provides a UI for configuring these operators.

Aggregations

Besides the MeasureAggregationOperator used for individual measures, the system also supports group aggregations through the GroupAggregationOperator:
export enum GroupAggregationOperator {
  Count = "COUNT",
  Average = "AVERAGE",
  Max = "MAX",
  Min = "MIN",
  Sum = "SUM",
}
This allows for complex data analysis across groups of measures.