-
Notifications
You must be signed in to change notification settings - Fork 2
Add grouping logic for voltage disturbances #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,7 @@ | |
| // Generated original version of source code. | ||
| // | ||
| //****************************************************************************************************** | ||
| import React from 'react'; | ||
| import * as React from 'react'; | ||
| import moment from 'moment'; | ||
| import { Table, Column } from '@gpa-gemstone/react-table'; | ||
| import { EventWidget } from '../global'; | ||
|
|
@@ -37,16 +37,25 @@ interface IDisturbanceData { | |
| StartTime: string; | ||
| SeverityCode: string; | ||
| IsWorstDisturbance: boolean; | ||
| GroupNumber?: number; | ||
| GroupColor?: string; | ||
| IsFirstGroupRow?: boolean; | ||
| IsLastGroupRow?: boolean; | ||
| } | ||
|
|
||
| const GROUP_COLORS = ['var(--blue)', 'var(--orange)', 'var(--green)', 'var(--purple)', 'var(--red)', 'var(--teal)', 'var(--pink)', 'var(--indigo)']; | ||
|
|
||
| /** Displays voltage disturbances for the selected event. */ | ||
| const AssetVoltageDisturbances: EventWidget.IWidget<{}> = { | ||
| Name: 'VoltageDisturbances', | ||
| DefaultSettings: {}, | ||
| Settings: () => <></>, | ||
| Widget: (props: EventWidget.IWidgetProps<{}>) => { | ||
| const [data, setData] = React.useState<IDisturbanceData[]>([]); | ||
| const [status, setStatus] = React.useState<Application.Types.Status>('uninitiated'); | ||
| const groupedData = React.useMemo(() => groupDisturbances(data), [data]); | ||
|
|
||
| // Load voltage disturbances whenever the selected event or application path changes. | ||
| React.useEffect(() => { | ||
| setStatus('loading'); | ||
| const handle = getDisturbanceData(props.HomePath, props.EventID); | ||
|
|
@@ -67,7 +76,7 @@ const AssetVoltageDisturbances: EventWidget.IWidget<{}> = { | |
| <Alert Class='alert-danger'> | ||
| An error occurred while fetching voltage disturbance data. | ||
| </Alert> | ||
| : null} | ||
| : null} | ||
| {status === 'loading' ? | ||
|
|
||
| <div className='d-flex align-items-center justify-content-center' style={{ height: 250 }}> | ||
|
|
@@ -77,84 +86,212 @@ const AssetVoltageDisturbances: EventWidget.IWidget<{}> = { | |
| <Alert Class='alert-info'> | ||
| No voltage disturbance data. | ||
| </Alert> | ||
| : | ||
| <Table<IDisturbanceData> | ||
| Data={data} | ||
| KeySelector={(item) => item.ID} | ||
| OnSort={() => {/*Do Nothing*/ }} | ||
| SortKey={''} | ||
| Ascending={true} | ||
| TableClass="table" | ||
| TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%', height: 50 }} | ||
| TbodyStyle={{ display: 'block', overflowY: 'auto', width: '100%', maxHeight: props.MaxHeight ?? 500 }} | ||
| RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} | ||
| Selected={(r) => r.IsWorstDisturbance} | ||
| > | ||
| <Column<IDisturbanceData> | ||
| Key={'EventType'} | ||
| AllowSort={false} | ||
| Field={'EventType'} | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| > | ||
| Disturbance Type | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
| Key={'Phase'} | ||
| AllowSort={false} | ||
| Field={'Phase'} | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| > | ||
| Phase | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
| Key={'PerUnitMagnitude'} | ||
| AllowSort={false} | ||
| Field={'PerUnitMagnitude'} | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| Content={row => (row.item.PerUnitMagnitude * 100).toFixed(1)} | ||
| > | ||
| Magnitude (%) | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
| Key={'DurationSeconds'} | ||
| AllowSort={false} | ||
| Field={'DurationSeconds'} | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| Content={row => (row.item.DurationSeconds * 1000).toFixed(2)} | ||
| : | ||
| <Table<IDisturbanceData> | ||
| Data={groupedData} | ||
| KeySelector={(item) => item.ID} | ||
| OnSort={() => {/*Do Nothing*/ }} | ||
| SortKey={''} | ||
| Ascending={true} | ||
| TableClass='table' | ||
| TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%', height: 50 }} | ||
| TbodyStyle={{ display: 'block', overflowY: 'auto', width: '100%', maxHeight: props.MaxHeight ?? 500 }} | ||
| RowStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} | ||
| > | ||
| Duration (ms) | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
| Key={'StartTime'} | ||
| AllowSort={false} | ||
| Field={'StartTime'} | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| Content={row => moment(row.item.StartTime).format('HH:mm:ss.SSS')} | ||
| > | ||
| Start Time | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
| Key={'SeverityCode'} | ||
| AllowSort={false} | ||
| Field={'SeverityCode'} | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| > | ||
| Severity | ||
| </Column> | ||
| </Table> | ||
| <Column<IDisturbanceData> | ||
| Key='GroupStatus' | ||
| AllowSort={false} | ||
| HeaderStyle={{ width: 180 }} | ||
| RowStyle={{ width: 180 }} | ||
| Content={({ item, style }) => { | ||
| applyGroupCellStyle(item, style, 'left'); | ||
| return <> | ||
| {item.GroupNumber != null ? | ||
| <span | ||
| className='badge mr-1' | ||
| style={{ backgroundColor: item.GroupColor, color: 'white' }} | ||
| > | ||
| {item.EventType} Group {item.GroupNumber} | ||
| </span> | ||
| : null} | ||
| {item.IsWorstDisturbance ? | ||
| <span className='badge badge-warning'>Worst</span> | ||
| : null} | ||
| </>; | ||
| }} | ||
| > | ||
| Group / Status | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the EventType will show up twice with this. (see example) seems unneccesarry |
||
| Key='EventType' | ||
| AllowSort={false} | ||
| Field='EventType' | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| Content={({ item, style }) => { | ||
| applyGroupCellStyle(item, style); | ||
| return item.EventType; | ||
| }} | ||
| > | ||
| Disturbance Type | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
| Key='Phase' | ||
| AllowSort={false} | ||
| Field='Phase' | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| Content={({ item, style }) => { | ||
| applyGroupCellStyle(item, style); | ||
| return item.Phase; | ||
| }} | ||
| > | ||
| Phase | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
| Key='PerUnitMagnitude' | ||
| AllowSort={false} | ||
| Field='PerUnitMagnitude' | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| Content={({ item, style }) => { | ||
| applyGroupCellStyle(item, style); | ||
| return (item.PerUnitMagnitude * 100).toFixed(1); | ||
| }} | ||
| > | ||
| Magnitude (%) | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
| Key='DurationSeconds' | ||
| AllowSort={false} | ||
| Field='DurationSeconds' | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| Content={({ item, style }) => { | ||
| applyGroupCellStyle(item, style); | ||
| return (item.DurationSeconds * 1000).toFixed(2); | ||
| }} | ||
| > | ||
| Duration (ms) | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
| Key='StartTime' | ||
| AllowSort={false} | ||
| Field='StartTime' | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| Content={({ item, style }) => { | ||
| applyGroupCellStyle(item, style); | ||
| return moment(item.StartTime).format('HH:mm:ss.SSS'); | ||
| }} | ||
| > | ||
| Start Time | ||
| </Column> | ||
| <Column<IDisturbanceData> | ||
| Key='SeverityCode' | ||
| AllowSort={false} | ||
| Field='SeverityCode' | ||
| HeaderStyle={{ width: 'auto' }} | ||
| RowStyle={{ width: 'auto' }} | ||
| Content={({ item, style }) => { | ||
| applyGroupCellStyle(item, style, 'right'); | ||
| return item.SeverityCode; | ||
| }} | ||
| > | ||
| Severity | ||
| </Column> | ||
| </Table> | ||
| } | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| interface IDisturbanceBucket { | ||
| Rows: IDisturbanceData[]; | ||
| StartTime: number; | ||
| EndTime: number; | ||
| } | ||
|
|
||
| /** Groups disturbances of the same event type into contiguous display blocks, including singleton groups. */ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not quite right. Type is not really relevant for the grouping, |
||
| export const groupDisturbances = (data: IDisturbanceData[]): IDisturbanceData[] => { | ||
| const rowsByType = new Map<string, IDisturbanceData[]>(); | ||
|
|
||
| data.forEach(row => { | ||
| const rows = rowsByType.get(row.EventType) ?? []; | ||
| rows.push(row); | ||
| rowsByType.set(row.EventType, rows); | ||
| }); | ||
|
Comment on lines
+221
to
+225
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sepperates by Disturbance Type? That should not be neccesarry |
||
|
|
||
| const blocks: IDisturbanceBucket[] = []; | ||
| rowsByType.forEach(rows => { | ||
| const sortedRows = [...rows].sort(compareDisturbances); | ||
| let currentBlock: IDisturbanceBucket | undefined; | ||
|
|
||
| sortedRows.forEach(row => { | ||
| const startTime = getStartTime(row); | ||
| const endTime = startTime + row.DurationSeconds * 1000; | ||
|
|
||
| if (currentBlock == null || startTime > currentBlock.EndTime) { | ||
| currentBlock = { Rows: [row], StartTime: startTime, EndTime: endTime }; | ||
| blocks.push(currentBlock); | ||
| } | ||
| else { | ||
| currentBlock.Rows.push(row); | ||
| currentBlock.EndTime = Math.max(currentBlock.EndTime, endTime); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| blocks.sort((left, right) => left.StartTime - right.StartTime); | ||
|
|
||
| const groupNumbersByType = new Map<string, number>(); | ||
| let groupColorIndex = 0; | ||
| return blocks.reduce<IDisturbanceData[]>((result, block) => { | ||
| const eventType = block.Rows[0].EventType; | ||
| const groupNumber = (groupNumbersByType.get(eventType) ?? 0) + 1; | ||
| groupNumbersByType.set(eventType, groupNumber); | ||
|
|
||
| const groupColor = GROUP_COLORS[groupColorIndex % GROUP_COLORS.length]; | ||
| groupColorIndex++; | ||
| block.Rows.forEach((row, index) => result.push({ | ||
| ...row, | ||
| GroupNumber: groupNumber, | ||
| GroupColor: groupColor, | ||
| IsFirstGroupRow: index === 0, | ||
| IsLastGroupRow: index === block.Rows.length - 1 | ||
| })); | ||
| return result; | ||
| }, []); | ||
| } | ||
|
|
||
| /** Sorts disturbances by start time while preserving input order for equal starts. */ | ||
| const compareDisturbances = (left: IDisturbanceData, right: IDisturbanceData): number => { | ||
| return getStartTime(left) - getStartTime(right); | ||
| } | ||
|
|
||
| /** Converts a disturbance start time to milliseconds. */ | ||
| const getStartTime = (row: IDisturbanceData): number => moment.utc(row.StartTime).valueOf(); | ||
|
|
||
| /** Applies grouped-row borders to the style object Gemstone passes from Column.Content to its td. */ | ||
| const applyGroupCellStyle = (row: IDisturbanceData, style?: React.CSSProperties, columnEdge?: 'left' | 'right'): void => { | ||
| if (row.GroupColor == null || style == null) return; | ||
|
|
||
| if (row.IsFirstGroupRow) | ||
| style.borderTop = `2px solid ${row.GroupColor}`; | ||
|
|
||
| if (row.IsLastGroupRow) | ||
| style.borderBottom = `2px solid ${row.GroupColor}`; | ||
|
|
||
| if (columnEdge === 'left') | ||
| style.borderLeft = `2px solid ${row.GroupColor}`; | ||
|
|
||
| if (columnEdge === 'right') | ||
| style.borderRight = `2px solid ${row.GroupColor}`; | ||
| } | ||
|
|
||
| /** Requests voltage disturbances for an event. */ | ||
| const getDisturbanceData = (homePath: string, eventID: number) => { | ||
| return $.ajax({ | ||
| type: "GET", | ||
|
|
@@ -166,4 +303,4 @@ const getDisturbanceData = (homePath: string, eventID: number) => { | |
| }); | ||
| } | ||
|
|
||
| export default AssetVoltageDisturbances; | ||
| export default AssetVoltageDisturbances; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we have the
gpa-colorsin here?