-
Notifications
You must be signed in to change notification settings - Fork 11
feat: grouped cartesian legend #1288
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
Changes from 37 commits
a3be401
1fe78fe
1fbd6c9
b53fe40
747556e
09513b3
1781dee
a33b271
805d7e1
7da7be2
0244802
458fa52
43c206e
0d2b82e
c9df216
9f5f84b
408b490
af35f00
967346e
d9be399
53e46bd
72d2af7
863d65d
902c0d8
a9b166b
c78844b
c2f177b
2669bb7
8ef7d2e
316f563
6b39400
5c57a97
5f5aafe
3572db1
a469833
3387b08
37efef0
f8b591a
f243622
3d585ba
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 |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { ComponentRef, Injector } from '@angular/core'; | ||
| import { Color, DynamicComponentService } from '@hypertrace/common'; | ||
| import { Color, Dictionary, DynamicComponentService } from '@hypertrace/common'; | ||
| import { ContainerElement, EnterElement, select, Selection } from 'd3-selection'; | ||
| import { groupBy, isNil } from 'lodash-es'; | ||
| import { Observable, Subject } from 'rxjs'; | ||
| import { startWith } from 'rxjs/operators'; | ||
| import { LegendPosition } from '../../../legend/legend.component'; | ||
|
|
@@ -24,7 +25,10 @@ export class CartesianLegend<TData> { | |
| public readonly activeSeries$: Observable<Series<TData>[]>; | ||
| private readonly activeSeriesSubject: Subject<Series<TData>[]> = new Subject(); | ||
| private readonly initialSeries: Series<TData>[]; | ||
| private readonly groupedSeries: Dictionary<Series<TData>[]>; | ||
|
|
||
| private readonly isGrouped: boolean = true; | ||
| private readonly isNonTitledGrouped: boolean = false; | ||
| private isSelectionModeOn: boolean = false; | ||
| private legendElement?: HTMLDivElement; | ||
| private activeSeries: Series<TData>[]; | ||
|
|
@@ -37,6 +41,10 @@ export class CartesianLegend<TData> { | |
| private readonly intervalData?: CartesianIntervalData, | ||
| private readonly summaries: Summary[] = [] | ||
| ) { | ||
| this.isGrouped = !isNil(this.series[0]?.groupName); | ||
| this.isNonTitledGrouped = this.series.length > 0 && this.series[0].name === this.series[0].groupName; | ||
|
||
| this.groupedSeries = this.isGrouped ? groupBy(this.series, seriesEntry => seriesEntry.groupName) : {}; | ||
|
|
||
| this.activeSeries = [...this.series]; | ||
| this.initialSeries = [...this.series]; | ||
| this.activeSeries$ = this.activeSeriesSubject.asObservable().pipe(startWith(this.series)); | ||
|
|
@@ -83,11 +91,44 @@ export class CartesianLegend<TData> { | |
| } | ||
|
|
||
| private drawLegendEntries(container: ContainerElement): void { | ||
| select(container) | ||
| const containerSelection = select(container); | ||
| if (!this.isGrouped) { | ||
| containerSelection | ||
| .append('div') | ||
| .classed('legend-entries', true) | ||
| .selectAll('.legend-entry') | ||
| .data(this.series.filter(series => !series.hide)) | ||
| .enter() | ||
| .each((_, index, elements) => this.drawLegendEntry(elements[index])); | ||
| } else { | ||
| containerSelection | ||
| .selectAll('.legend-entries') | ||
anandtiwary marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .data(Object.values(this.groupedSeries)) | ||
| .enter() | ||
| .append('div') | ||
| .attr('class', (_, index) => `legend-entries group-${index + 1}`) | ||
|
||
| .each((seriesGroup, index, elements) => this.drawLegendEntriesTitleAndValues(seriesGroup, elements[index])); | ||
| } | ||
| } | ||
|
|
||
| private drawLegendEntriesTitleAndValues(seriesGroup: Series<TData>[], element: HTMLDivElement): void { | ||
| const legendEntriesSelection = select(element); | ||
| if (!this.isNonTitledGrouped) { | ||
| legendEntriesSelection | ||
| .selectAll('.legend-entries-title') | ||
| .data([seriesGroup]) | ||
| .enter() | ||
| .append('div') | ||
| .classed('legend-entries-title', true) | ||
| .text(group => `${group[0].groupName}:`) | ||
| .on('click', () => this.updateActiveSeriesGroup(seriesGroup)); | ||
| } | ||
|
|
||
| legendEntriesSelection | ||
itssharmasandeep marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .append('div') | ||
| .classed('legend-entries', true) | ||
| .classed('legend-entry-values', true) | ||
| .selectAll('.legend-entry') | ||
| .data(this.series.filter(series => !series.hide)) | ||
| .data(seriesGroup) | ||
| .enter() | ||
| .each((_, index, elements) => this.drawLegendEntry(elements[index])); | ||
| } | ||
|
|
@@ -107,7 +148,8 @@ export class CartesianLegend<TData> { | |
| return select(hostElement) | ||
| .append('div') | ||
| .classed(CartesianLegend.CSS_CLASS, true) | ||
| .classed(`position-${legendPosition}`, true); | ||
| .classed(`position-${legendPosition}`, true) | ||
| .classed('grouped', this.isGrouped); | ||
| } | ||
|
|
||
| private drawLegendEntry(element: EnterElement): Selection<HTMLDivElement, Series<TData>, null, undefined> { | ||
|
|
@@ -128,6 +170,21 @@ export class CartesianLegend<TData> { | |
|
|
||
| private updateLegendClassesAndStyle(): void { | ||
| const legendElementSelection = select(this.legendElement!); | ||
| if (this.isGrouped) { | ||
| // Legend entries | ||
| select(this.legendElement!) | ||
| .selectAll('.legend-entries') | ||
| .classed(CartesianLegend.ACTIVE_CSS_CLASS, seriesGroup => | ||
| this.isThisLegendSeriesGroupActive(seriesGroup as Series<TData>[]) | ||
| ); | ||
|
|
||
| // Legend entry title | ||
| select(this.legendElement!) | ||
| .selectAll('.legend-entries-title') | ||
| .classed(CartesianLegend.ACTIVE_CSS_CLASS, seriesGroup => | ||
| this.isThisLegendSeriesGroupActive(seriesGroup as Series<TData>[]) | ||
| ); | ||
| } | ||
|
|
||
| // Legend entry symbol | ||
| legendElementSelection | ||
|
|
@@ -201,14 +258,29 @@ export class CartesianLegend<TData> { | |
| this.activeSeriesSubject.next(this.activeSeries); | ||
| } | ||
|
|
||
| private updateActiveSeries(seriesEntry: Series<TData>): void { | ||
| private updateActiveSeriesGroup(seriesGroup: Series<TData>[]): void { | ||
| if (!this.isSelectionModeOn) { | ||
| this.activeSeries = [seriesEntry]; | ||
| this.activeSeries = [...seriesGroup]; | ||
| this.isSelectionModeOn = true; | ||
| } else if (this.isThisLegendEntryActive(seriesEntry)) { | ||
| this.activeSeries = this.activeSeries.filter(series => series !== seriesEntry); | ||
| } else if (!this.isThisLegendSeriesGroupActive(seriesGroup)) { | ||
| this.activeSeries = this.activeSeries.filter(series => !seriesGroup.includes(series)); | ||
| this.activeSeries.push(...seriesGroup); | ||
| } else { | ||
| this.activeSeries.push(seriesEntry); | ||
| this.activeSeries = this.activeSeries.filter(series => !seriesGroup.includes(series)); | ||
| } | ||
| this.updateLegendClassesAndStyle(); | ||
| this.updateResetElementVisibility(!this.isSelectionModeOn); | ||
| this.activeSeriesSubject.next(this.activeSeries); | ||
| } | ||
|
|
||
| private updateActiveSeries(series: Series<TData>): void { | ||
| if (!this.isSelectionModeOn) { | ||
| this.activeSeries = [series]; | ||
| this.isSelectionModeOn = true; | ||
| } else if (this.isThisLegendEntryActive(series)) { | ||
| this.activeSeries = this.activeSeries.filter(seriesEntry => series !== seriesEntry); | ||
| } else { | ||
| this.activeSeries.push(series); | ||
| } | ||
| this.updateLegendClassesAndStyle(); | ||
| this.updateResetElementVisibility(!this.isSelectionModeOn); | ||
|
|
@@ -218,4 +290,8 @@ export class CartesianLegend<TData> { | |
| private isThisLegendEntryActive(seriesEntry: Series<TData>): boolean { | ||
| return this.activeSeries.includes(seriesEntry); | ||
| } | ||
|
|
||
| private isThisLegendSeriesGroupActive(seriesGroup: Series<TData>[]): boolean { | ||
| return !this.isSelectionModeOn ? false : seriesGroup.every(series => this.activeSeries.includes(series)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import { ColorService, forkJoinSafeEmpty, RequireBy, TimeDuration } from '@hypertrace/common'; | ||
| import { ModelInject } from '@hypertrace/hyperdash-angular'; | ||
| import { isEmpty } from 'lodash-es'; | ||
| import { isNil } from 'lodash-es'; | ||
| import { NEVER, Observable, of } from 'rxjs'; | ||
| import { map, mergeMap } from 'rxjs/operators'; | ||
| import { Series } from '../../../../components/cartesian/chart'; | ||
|
|
@@ -134,11 +134,12 @@ export abstract class ExploreCartesianDataSourceModel extends GraphQlDataSourceM | |
| data: result.data, | ||
| units: obj.attribute.units !== '' ? obj.attribute.units : undefined, | ||
| type: request.series.find(series => series.specification === result.spec)!.visualizationOptions.type, | ||
| name: isEmpty(result.groupName) | ||
| ? obj.specDisplayName | ||
| : request.useGroupName | ||
| ? result.groupName! | ||
| : `${obj.specDisplayName}: ${result.groupName}`, | ||
| name: result.groupName ?? obj.specDisplayName, | ||
| groupName: !isNil(result.groupName) | ||
|
||
| ? request.useGroupName | ||
| ? result.groupName | ||
| : obj.specDisplayName | ||
| : undefined, | ||
| color: color | ||
| })) | ||
| ); | ||
|
|
||
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.
We had discussed getting rid of this too, and having a single concept of a series group, which could contain from 1:N series. The purpose of that was to avoid all the conditional branching for grouped vs non grouped behavior in this class. Can defer.