Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -118,13 +118,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 {
@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,8 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {
onEvent: ChartEventListener<TData>;
}[] = [];

private activeSeriesSubscription?: Subscription;

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

if (this.activeSeriesSubscription) {
this.activeSeriesSubscription.unsubscribe();
}

return this;
}

Expand Down Expand Up @@ -271,8 +279,10 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {
}
}

private updateData(): void {
this.drawLegend();
private updateData(withLegend: boolean = true): void {
if (withLegend) {
this.drawLegend();
}
this.buildVisualizations();
this.drawChartBackground();
this.drawAxes();
Expand All @@ -283,6 +293,16 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {
this.setupEventListeners();
}

private redrawVisualization(): void {
const chartViz = select(this.chartContainerElement!).selectAll(
`.${DefaultCartesianChart.CHART_VISUALIZATION_CLASS}`
);
if (chartViz.nodes().length > 0) {
chartViz.remove();
this.updateData(false);
}
}

private moveDataOnTopOfAxes(): void {
if (!this.dataElement) {
return;
Expand Down Expand Up @@ -348,6 +368,14 @@ export class DefaultCartesianChart<TData> implements CartesianChart<TData> {
this.chartContainerElement,
this.legendPosition
);
if (this.activeSeriesSubscription) {
this.activeSeriesSubscription.unsubscribe();
}
this.activeSeriesSubscription = this.legend.activeSeries$.subscribe(activeSeries => {
this.series.length = 0;
this.series.push(...(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 +398,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
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 { 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<{}>[]>;
private readonly activeSeriesSubject: Subject<void> = new Subject();
private readonly initialSeries: Series<{}>[];

private isDefault: boolean = true;
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];
this.initialSeries = [...this.series];
this.activeSeries$ = this.activeSeriesSubject.asObservable().pipe(
startWith(undefined),
switchMap(() => of(this.activeSeries))
);
}

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.resetToDefault());

this.toggleReset(this.isDefault);
}

private toggleReset(isHidden: boolean): void {
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,31 @@ 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));

this.updateLegendTextClasses();

return legendEntry;
}

private updateLegendTextClasses(): void {
select(this.legendElement!)
.selectAll('span.legend-text')
.classed(CartesianLegend.DEFAULT_CSS_CLASS, this.isDefault)
.classed(
CartesianLegend.ACTIVE_CSS_CLASS,
series => !this.isDefault && this.isThisLegendEntryActive(series as Series<{}>)
)
.classed(
CartesianLegend.INACTIVE_CSS_CLASS,
series => !this.isDefault && !this.isThisLegendEntryActive(series as Series<{}>)
);
}

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

private resetToDefault(): void {
this.activeSeries = [...this.initialSeries];
this.isDefault = true;
this.updateLegendTextClasses();
this.toggleReset(this.isDefault);
this.activeSeriesSubject.next();
}

private updateActiveSeries(seriesEntry: Series<{}>): void {
if (this.isDefault) {
this.activeSeries = [];
this.activeSeries.push(seriesEntry);
this.isDefault = false;
} else {
if (this.isThisLegendEntryActive(seriesEntry)) {
this.activeSeries = this.activeSeries.filter(series => series !== seriesEntry);
} else {
this.activeSeries.push(seriesEntry);
}
}
this.updateLegendTextClasses();
this.toggleReset(this.isDefault);
this.activeSeriesSubject.next();
}

private isThisLegendEntryActive(seriesEntry: Series<{}>): boolean {
return this.activeSeries.findIndex(series => series === seriesEntry) >= 0;
}
}