Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
width: 100%;
position: absolute;
min-height: 48px;
padding-bottom: 20px;

&.position-none {
display: none;
Expand Down Expand Up @@ -118,13 +119,41 @@
}

.legend-text {
fill: $gray-5;
font-size: 14px;
padding-left: 2px;
cursor: pointer;

&.default {
color: $gray-9;
}

&.active {
color: $blue-4;
}

&.inactive {
color: $gray-5;
}
}
}
}

.reset {
Comment thread
itssharmasandeep marked this conversation as resolved.
@include font-title($blue-4);
cursor: pointer;
position: absolute;
bottom: 0;
right: 0;

&.hidden {
display: none;
}

&:hover {
color: $blue-6;
}
}

.interval-control {
padding: 0 8px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,47 @@ describe('Cartesian Chart component', () => {
expect(chart.queryAll(CartesianLegend.CSS_SELECTOR, { root: true }).length).toBe(1);
}));

test('should have correct active series', fakeAsync(() => {
const chart = createHost(`<ht-cartesian-chart [series]="series" [legend]="legend"></ht-cartesian-chart>`, {
hostProps: {
series: [],
legend: undefined
}
});
chart.setHostInput({
series: [
{
data: [[1, 2]],
name: 'test series 1',
color: 'blue',
type: CartesianSeriesVisualizationType.Column,
stacking: true
},
{
data: [[1, 6]],
name: 'test series 2',
color: 'red',
type: CartesianSeriesVisualizationType.Column,
stacking: true
}
],
legend: LegendPosition.Bottom
});
tick();
expect(chart.queryAll(CartesianLegend.CSS_SELECTOR, { root: true }).length).toBe(1);
expect(chart.queryAll('.legend-entry').length).toBe(2);
expect(chart.query('.reset.hidden')).toExist();

const legendEntryTexts = chart.queryAll('.legend-text');
chart.click(legendEntryTexts[0]);
tick();
expect(chart.query('.reset.hidden')).not.toExist();

chart.click(chart.query('.reset') as Element);
tick();
expect(chart.query('.reset.hidden')).toExist();
}));

test('should render column chart', fakeAsync(() => {
const chart = createHost(`<ht-cartesian-chart [series]="series"></ht-cartesian-chart>`, {
hostProps: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injector, Renderer2 } from '@angular/core';
import { TimeRange } from '@hypertrace/common';
import { ContainerElement, mouse, select } from 'd3-selection';
import { Subscription } from 'rxjs';
import { LegendPosition } from '../../../legend/legend.component';
import { ChartTooltipRef } from '../../../utils/chart-tooltip/chart-tooltip-popover';
import { D3UtilService } from '../../../utils/d3/d3-util.service';
Expand Down Expand Up @@ -35,6 +36,7 @@ import { CartesianScaleBuilder } from '../scale/cartesian-scale-builder';
// tslint:disable:max-file-line-count
export class DefaultCartesianChart<TData> implements CartesianChart<TData> {
public static DATA_SERIES_CLASS: string = 'data-series';
public static CHART_VISUALIZATION_CLASS: string = 'chart-visualization';

protected readonly margin: number = 16;
protected readonly axisHeight: number = 16;
Expand Down Expand Up @@ -65,6 +67,9 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {
onEvent: ChartEventListener<TData>;
}[] = [];

private activeSeriesSubscription?: Subscription;
private activeSeries: Series<TData>[] = [];

public constructor(
protected readonly hostElement: Element,
protected readonly injector: Injector,
Expand All @@ -80,6 +85,10 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {
this.tooltip && this.tooltip.destroy();
this.legend && this.legend.destroy();

if (this.activeSeriesSubscription) {
Comment thread
itssharmasandeep marked this conversation as resolved.
this.activeSeriesSubscription.unsubscribe();
}

return this;
}

Expand All @@ -104,6 +113,7 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {
public withSeries(...series: Series<TData>[]): this {
this.series.length = 0;
this.series.push(...series);
this.activeSeries = [...series];

this.seriesSummaries.length = 0;
this.seriesSummaries.push(
Expand Down Expand Up @@ -273,6 +283,10 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {

private updateData(): void {
this.drawLegend();
this.drawVisualizations();
}

private drawVisualizations(): void {
this.buildVisualizations();
this.drawChartBackground();
this.drawAxes();
Expand All @@ -283,6 +297,16 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {
this.setupEventListeners();
}

private redrawVisualization(): void {
const chartViz = select(this.chartContainerElement!).selectAll(
Comment thread
aaron-steinfeld marked this conversation as resolved.
`.${DefaultCartesianChart.CHART_VISUALIZATION_CLASS}`
);
if (chartViz.nodes().length > 0) {
chartViz.remove();
this.drawVisualizations();
}
}

private moveDataOnTopOfAxes(): void {
if (!this.dataElement) {
return;
Expand Down Expand Up @@ -338,16 +362,23 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {
return;
}

new CartesianNoDataMessage(this.chartBackgroundSvgElement, this.series).updateMessage();
new CartesianNoDataMessage(this.chartBackgroundSvgElement, this.activeSeries).updateMessage();
}

private drawLegend(): void {
if (this.chartContainerElement) {
if (this.legendPosition !== undefined && this.legendPosition !== LegendPosition.None) {
this.legend = new CartesianLegend(this.series, this.injector, this.intervalData, this.seriesSummaries).draw(
this.chartContainerElement,
this.legendPosition
);
this.legend = new CartesianLegend(
this.activeSeries,
this.injector,
this.intervalData,
this.seriesSummaries
).draw(this.chartContainerElement, this.legendPosition);
this.activeSeriesSubscription?.unsubscribe();
this.activeSeriesSubscription = this.legend.activeSeries$.subscribe(activeSeries => {
Comment thread
itssharmasandeep marked this conversation as resolved.
this.activeSeries = activeSeries as Series<TData>[];
this.redrawVisualization();
});
} else {
// The legend also contains the interval selector, so even without a legend we need to create an element for that
this.legend = new CartesianLegend([], this.injector, this.intervalData, this.seriesSummaries).draw(
Expand All @@ -370,6 +401,7 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {

this.chartBackgroundSvgElement = select(this.chartContainerElement)
.append('svg')
.classed(DefaultCartesianChart.CHART_VISUALIZATION_CLASS, true)
.style('position', 'absolute')
.attr('width', `${chartBox.width}px`)
.attr('height', `${chartBox.height}px`)
Expand Down Expand Up @@ -430,7 +462,7 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {

private buildVisualizations(): void {
this.allSeriesData = [
...this.series.map(series => this.getChartSeriesVisualization(series)),
...this.activeSeries.map(series => this.getChartSeriesVisualization(series)),
...this.bands.flatMap(band => [
// Need to add bands as series to get tooltips
this.getChartSeriesVisualization(band.upper),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ComponentRef, Injector } from '@angular/core';
import { DynamicComponentService } from '@hypertrace/common';
import { Color, DynamicComponentService } from '@hypertrace/common';
import { ContainerElement, EnterElement, select, Selection } from 'd3-selection';
import { Observable, of, Subject } from 'rxjs';
import { startWith, switchMap } from 'rxjs/operators';
import { LegendPosition } from '../../../legend/legend.component';
import { Series, Summary } from '../../chart';
import {
Expand All @@ -12,9 +14,19 @@ import { CartesianSummaryComponent, SUMMARIES_DATA } from './cartesian-summary.c

export class CartesianLegend {
private static readonly CSS_CLASS: string = 'legend';
private static readonly RESET_CSS_CLASS: string = 'reset';
private static readonly DEFAULT_CSS_CLASS: string = 'default';
private static readonly ACTIVE_CSS_CLASS: string = 'active';
private static readonly INACTIVE_CSS_CLASS: string = 'inactive';
public static readonly CSS_SELECTOR: string = `.${CartesianLegend.CSS_CLASS}`;

public readonly activeSeries$: Observable<Series<{}>[]>;
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated
private readonly activeSeriesSubject: Subject<void> = new Subject();
private readonly initialSeries: Series<{}>[];

private isSelectionModeOn: boolean = false;
private legendElement?: HTMLDivElement;
private activeSeries: Series<{}>[];
private intervalControl?: ComponentRef<unknown>;
private summaryControl?: ComponentRef<unknown>;

Expand All @@ -23,7 +35,14 @@ export class CartesianLegend {
private readonly injector: Injector,
private readonly intervalData?: CartesianIntervalData,
private readonly summaries: Summary[] = []
) {}
) {
this.activeSeries = [...this.series];
Comment thread
itssharmasandeep marked this conversation as resolved.
this.initialSeries = [...this.series];
this.activeSeries$ = this.activeSeriesSubject.asObservable().pipe(
Comment thread
itssharmasandeep marked this conversation as resolved.
Outdated
startWith(undefined),
switchMap(() => of(this.activeSeries))
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated
);
}

public draw(hostElement: Element, position: LegendPosition): this {
this.legendElement = this.drawLegendContainer(hostElement, position, this.intervalData !== undefined).node()!;
Expand All @@ -33,6 +52,7 @@ export class CartesianLegend {
}

this.drawLegendEntries(this.legendElement);
this.drawReset(this.legendElement);

if (this.intervalData) {
this.intervalControl = this.drawIntervalControl(this.legendElement, this.intervalData);
Expand All @@ -50,6 +70,20 @@ export class CartesianLegend {
this.summaryControl && this.summaryControl.destroy();
}

private drawReset(container: ContainerElement): void {
select(container)
.append('span')
.classed(CartesianLegend.RESET_CSS_CLASS, true)
.text('Reset')
.on('click', () => this.makeSelectionModeOff());
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated

this.setResetVisibility(!this.isSelectionModeOn);
}

private setResetVisibility(isHidden: boolean): void {
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated
select(this.legendElement!).select(`span.${CartesianLegend.RESET_CSS_CLASS}`).classed('hidden', isHidden);
}

private drawLegendEntries(container: ContainerElement): void {
select(container)
.append('div')
Expand Down Expand Up @@ -82,15 +116,41 @@ export class CartesianLegend {
const legendEntry = select<EnterElement, Series<{}>>(element).append('div').classed('legend-entry', true);

this.appendLegendSymbol(legendEntry);

legendEntry
.append('span')
.classed('legend-text', true)
.text(series => series.name);
.text(series => series.name)
.on('click', series => this.updateActiveSeries(series));
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated

this.updateLegendClassesAndStyle();

return legendEntry;
}

private updateLegendClassesAndStyle(): void {
const legendElementSelection = select(this.legendElement!);

// Legend entry symbol
legendElementSelection
.selectAll('.legend-symbol circle')
.style('fill', series =>
!this.isThisLegendEntryActive(series as Series<{}>) ? Color.Gray3 : (series as Series<{}>).color
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated
);

// Legend entry value text
legendElementSelection
.selectAll('span.legend-text')
.classed(CartesianLegend.DEFAULT_CSS_CLASS, !this.isSelectionModeOn)
.classed(
CartesianLegend.ACTIVE_CSS_CLASS,
series => this.isSelectionModeOn && this.isThisLegendEntryActive(series as Series<{}>)
)
.classed(
CartesianLegend.INACTIVE_CSS_CLASS,
series => this.isSelectionModeOn && !this.isThisLegendEntryActive(series as Series<{}>)
);
}

private appendLegendSymbol(selection: Selection<HTMLDivElement, Series<{}>, null, undefined>): void {
selection
.append('svg')
Expand Down Expand Up @@ -133,4 +193,33 @@ export class CartesianLegend {
})
);
}

private makeSelectionModeOff(): void {
this.activeSeries = [...this.initialSeries];
this.isSelectionModeOn = false;
this.updateLegendClassesAndStyle();
this.setResetVisibility(!this.isSelectionModeOn);
this.activeSeriesSubject.next();
}

private updateActiveSeries(seriesEntry: Series<{}>): void {
if (!this.isSelectionModeOn) {
this.activeSeries = [];
this.activeSeries.push(seriesEntry);
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated
this.isSelectionModeOn = true;
} else {
if (this.isThisLegendEntryActive(seriesEntry)) {
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated
this.activeSeries = this.activeSeries.filter(series => series !== seriesEntry);
} else {
this.activeSeries.push(seriesEntry);
}
}
this.updateLegendClassesAndStyle();
this.setResetVisibility(!this.isSelectionModeOn);
this.activeSeriesSubject.next();
}

private isThisLegendEntryActive(seriesEntry: Series<{}>): boolean {
return this.activeSeries.includes(seriesEntry);
}
}