AMBARI-22615 Log Search UI: improve histogram display. (Istvan Tobias via 
ababiichuk)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/b1fd131f
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/b1fd131f
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/b1fd131f

Branch: refs/heads/branch-3.0-perf
Commit: b1fd131f78feda8f9fef6f33dbc8c67e3fdf539f
Parents: ee50629
Author: Istvan Tobias <tobias.ist...@gmail.com>
Authored: Fri Dec 8 13:32:20 2017 +0200
Committer: ababiichuk <ababiic...@hortonworks.com>
Committed: Fri Dec 8 19:00:36 2017 +0200

----------------------------------------------------------------------
 .../src/app/classes/histogram-options.ts        |   3 +-
 .../src/app/components/mixins.less              |  18 +-
 .../time-histogram.component.html               |  29 ++
 .../time-histogram.component.less               | 162 +++++++-
 .../time-histogram.component.spec.ts            | 138 ++++++-
 .../time-histogram/time-histogram.component.ts  | 377 ++++++++++++++++---
 .../src/app/services/logs-container.service.ts  |   4 +-
 .../src/assets/i18n/en.json                     |  13 +-
 8 files changed, 669 insertions(+), 75 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts
----------------------------------------------------------------------
diff --git 
a/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts 
b/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts
index dee5d98..15fefde 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts
@@ -27,8 +27,7 @@ export interface HistogramStyleOptions {
   margin?: HistogramMarginOptions;
   height?: number;
   tickPadding?: number;
-  columnWidth?: number;
-  dragAreaColor?: string;
+  columnWidth?: {[key:string]: number};
 }
 
 export interface HistogramOptions extends HistogramStyleOptions {

http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less
----------------------------------------------------------------------
diff --git 
a/ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less 
b/ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less
index a6e5616..890887a 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less
@@ -203,25 +203,11 @@
   display: inline-block;
   width: 0;
   height: 0;
-  margin-left: @caret-width * .85;
   vertical-align: @caret-width * .85;
   content: "";
   .caret-direction(@caret-width, @direction, @color);
 }
 // This is the main caret mixin to create the common and the direction related 
css
-.caret(@caret-width; @direction: down; @color: @base-font-color; @position: 
before) {
-
-  &::before when (@position = before) {
-    .caret-style(@caret-width, @direction, @color);
-  }
-  &::after when (@position = after) {
-    .caret-style(@caret-width, @direction, @color);
-  }
-
-  &:empty::before when (@position = before) {
-    margin-left: 0;
-  }
-  &:empty::after when (@position = after) {
-    margin-left: 0;
-  }
+.caret-mixin(@caret-width; @direction: down; @color: @base-font-color; 
@position: before) {
+  .caret-style(@caret-width, @direction, @color);
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
----------------------------------------------------------------------
diff --git 
a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
 
b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
index 299e46e..1193b2e 100644
--- 
a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
+++ 
b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
@@ -15,4 +15,33 @@
   limitations under the License.
 -->
 
+<header>
+  <div class="container-fluid">
+    <div class="row">
+      <div *ngIf="chartTimeGap" class="time-gap col-lg-2 col-md-offset-5">
+        {{chartTimeGap.value}} {{chartTimeGap.label | translate}} 
{{'histogram.gap' | translate}}
+      </div>
+      <div class="legends col-md-5" [class.md-offset-7]="!chartTimeGap">
+        <div *ngFor="let legend of legends" class="legend {{legend.level | 
lowercase}}">
+          {{ legend.label | translate }}
+        </div>
+      </div>
+    </div>
+  </div>
+</header>
 <div #container></div>
+<footer *ngIf="firstDateTick || lastDateTick">
+  <div *ngIf="firstDateTick" class="first-date-tick-label">{{firstDateTick | 
amTz:timeZone | amDateFormat:historyStartEndTimeFormat}}</div>
+  <div *ngIf="lastDateTick" class="last-date-tick-label">{{lastDateTick | 
amTz:timeZone | amDateFormat:historyStartEndTimeFormat}}</div>
+</footer>
+<div [ngClass]="{hide: !tooltipInfo, 'tooltip-left': tooltipOnTheLeft, 
'tooltip-chart': true}" #tooltipEl
+     [style.top]="tooltipInfo ? (tooltipPosition.top + 'px') : ''" 
[style.left]="tooltipInfo ? (tooltipPosition.left + 'px') : ''">
+  <ng-container *ngIf="tooltipInfo">
+    <div class="tooltip-chart-date">{{tooltipInfo.timeStamp | amTz:timeZone | 
amDateFormat:tickTimeFormat}}</div>
+    <div *ngFor="let data of tooltipInfo.data" class="level {{data.level | 
lowercase}}">
+      <span class="level-label">{{data.levelLabel | translate }}</span>
+      <span class="level-value">{{data.value}}</span>
+    </div>
+  </ng-container>
+</div>
+

http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
----------------------------------------------------------------------
diff --git 
a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
 
b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
index e8d3240..1d3766d 100644
--- 
a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
+++ 
b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
@@ -16,25 +16,179 @@
  * limitations under the License.
  */
 
+@import '../mixins';
+
 :host {
-  display: block;
-  cursor: crosshair;
+  position: relative;
+  .level-mixin(@level, @size: .8em) {
+    @name: "@{level}-color";
+    border-radius: 100%;
+    content: "";
+    display: inline-block;
+    height: .8em;
+    width: .8em;
+    background-color: @@name;
+  }
+
   background: #ECECEC; // TODO add style according to actual design
+  display: block;
+
   /deep/ .axis {
     .domain {
       display: none;
     }
-
     .tick {
       cursor: default;
-
       line {
         display: none;
       }
     }
   }
 
+  /deep/ svg {
+    cursor: crosshair;
+  }
+
   /deep/ .value {
     cursor: pointer;
+    rect {
+      transition: opacity 250ms;
+      opacity: .8;
+      &:hover {
+        opacity: 1;
+      }
+    }
+  }
+
+  /deep/ .tooltip-chart {
+    background: #fff;
+    border-radius: 4px;
+    border: @input-border;
+    display: block;
+    font-size: .8em;
+    margin: 0 1.5em;
+    min-height: 2em;
+    min-width: 5em;
+    padding: .5em;
+    position: absolute;
+    &:empty {
+      display: none;
+    }
+    &::before {
+      .caret-mixin(6px, left, #fff);
+      left: -6px;
+      position: absolute;
+      top: calc(50% - 2px);
+    }
+    &.tooltip-left::before {
+      display: none;
+    }
+    &.tooltip-left::after {
+      .caret-mixin(6px, right, #fff);
+      right: -6px;
+      position: absolute;
+      top: calc(50% - 2px);
+    }
+    .tooltip-chart-date {
+      padding: 0 0 .1em 0;
+      text-align: center;
+    }
+    .level {
+      display: flex;
+      &::before {
+        margin: auto .2em auto 0;
+      }
+      .level-label {
+        flex-grow: 3;
+        padding: 0 2em 0 0;
+      }
+      .level-value {
+        text-align: right;
+      }
+    }
+
+    .fatal::before {
+      .level-mixin('fatal');
+    }
+    .error::before {
+      .level-mixin('error');
+    }
+    .warn::before {
+      .level-mixin('warning');
+    }
+    .info::before {
+      .level-mixin('info');
+    }
+    .trace::before {
+      .level-mixin('trace');
+    }
+    .debug::before {
+      .level-mixin('debug');
+    }
+    .unknown::before {
+      .level-mixin('unknown');
+    }
+  }
+  header {
+    padding: .5rem;
   }
+  .legends {
+    text-align: right;
+    .legend {
+      display: inline-block;
+      font-size: 1rem;
+      text-transform: uppercase;
+      padding-right: 1em;
+    }
+    .fatal::before {
+      .level-mixin('fatal');
+    }
+    .error::before {
+      .level-mixin('error');
+    }
+    .warn::before {
+      .level-mixin('warning');
+    }
+    .info::before {
+      .level-mixin('info');
+    }
+    .trace::before {
+      .level-mixin('trace');
+    }
+    .debug::before {
+      .level-mixin('debug');
+    }
+    .unknown::before {
+      .level-mixin('unknown');
+    }
+  }
+
+  .time-gap {
+    color: #666;
+    font-size: 1.2rem;
+    text-align: center;
+  }
+
+  footer {
+    display: flex;
+    div {
+      color: #666;
+      flex-grow: 1;
+      font-size: 1.2rem;
+      padding: 0 1em .5em 1em;
+    }
+    .last-date-tick-label {
+      text-align: right;
+    }
+  }
+
+  /deep/ rect.drag-area {
+    fill: #fff;
+  }
+
+  /deep/ rect.unselected-drag-area {
+    fill: darken(@main-background-color, 10%);
+    opacity: .6;
+  }
+
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts
----------------------------------------------------------------------
diff --git 
a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts
 
b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts
index 9e056be..ee14780 100644
--- 
a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts
+++ 
b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts
@@ -19,35 +19,157 @@
 import {async, ComponentFixture, TestBed} from '@angular/core/testing';
 import {StoreModule} from '@ngrx/store';
 import {AppSettingsService, appSettings} from 
'@app/services/storage/app-settings.service';
+import {TranslationModules} from '@app/test-config.spec';
+import {MomentModule} from 'angular2-moment';
+import {MomentTimezoneModule} from 'angular-moment-timezone';
+import {TimeZoneAbbrPipe} from '@app/pipes/timezone-abbr.pipe';
 
+import {ServiceLogsHistogramDataService} from 
'@app/services/storage/service-logs-histogram-data.service';
 import {TimeHistogramComponent} from './time-histogram.component';
+import {LogsContainerService} from '@app/services/logs-container.service';
+import {HttpClientService} from "@app/services/http-client.service";
+import {AppStateService} from "@app/services/storage/app-state.service";
+import {AuditLogsService} from "@app/services/storage/audit-logs.service";
+import {AuditLogsFieldsService} from 
"@app/services/storage/audit-logs-fields.service";
+import {ServiceLogsService} from "@app/services/storage/service-logs.service";
+import {ServiceLogsFieldsService} from 
"@app/services/storage/service-logs-fields.service";
+import {ServiceLogsTruncatedService} from 
"@app/services/storage/service-logs-truncated.service";
+import {TabsService} from "@app/services/storage/tabs.service";
+import {ClustersService} from "@app/services/storage/clusters.service";
+import {ComponentsService} from "@app/services/storage/components.service";
+import {HostsService} from "@app/services/storage/hosts.service";
 
 describe('TimeHistogramComponent', () => {
   let component: TimeHistogramComponent;
   let fixture: ComponentFixture<TimeHistogramComponent>;
+  let histogramData: any;
+  let customOptions: any;
 
   beforeEach(async(() => {
+    const httpClient = {
+      get: () => {
+        return {
+          subscribe: () => {}
+        }
+      }
+    };
+    histogramData = {
+      "1512476481940": {
+        "FATAL": 0,
+        "ERROR": 1000,
+        "WARN": 700,
+        "INFO": 0,
+        "DEBUG": 0,
+        "TRACE": 0,
+        "UNKNOWN": 0
+      }, "1512472881940": {"FATAL": 0, "ERROR": 2000, "WARN": 900, "INFO": 0, 
"DEBUG": 0, "TRACE": 0, "UNKNOWN": 0}
+    };
+    customOptions = {
+      keysWithColors: {
+        FATAL: '#830A0A',
+        ERROR: '#E81D1D',
+        WARN: '#FF8916',
+        INFO: '#2577B5',
+        DEBUG: '#65E8FF',
+        TRACE: '#888',
+        UNKNOWN: '#BDBDBD'
+      }
+    };
     TestBed.configureTestingModule({
-      declarations: [TimeHistogramComponent],
+      declarations: [TimeHistogramComponent, TimeZoneAbbrPipe],
       imports: [
         StoreModule.provideStore({
           appSettings
-        })
+        }),
+        ...TranslationModules,
+        MomentModule,
+        MomentTimezoneModule
       ],
       providers: [
-        AppSettingsService
+        AppSettingsService,
+        ServiceLogsHistogramDataService,
+        LogsContainerService,
+        {
+          provide: HttpClientService,
+          useValue: httpClient
+        },
+        AppStateService,
+        AuditLogsService,
+        AuditLogsFieldsService,
+        ServiceLogsService,
+        ServiceLogsFieldsService,
+        ServiceLogsHistogramDataService,
+        ServiceLogsTruncatedService,
+        TabsService,
+        ClustersService,
+        ComponentsService,
+        HostsService
       ]
     })
-    .compileComponents();
+      .compileComponents();
   }));
 
   beforeEach(() => {
-    fixture = TestBed.createComponent(TimeHistogramComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
+      fixture = TestBed.createComponent(TimeHistogramComponent);
+      component = fixture.componentInstance;
+      component.customOptions = customOptions;
+      component.svgId = "HistogramSvg";
+      component.data = histogramData;
+      fixture.detectChanges();
+    });
 
   it('should create component', () => {
     expect(component).toBeTruthy();
   });
+
+  const getTimeGapTestCases = [{
+    startDate: new Date(2017, 0, 1),
+    endDate: new Date(2017, 0, 8),
+    expected: {
+      unit: 'week',
+      value: 1,
+      label: 'histogram.gap.week'
+    }
+  }, {
+    startDate: new Date(2017, 0, 1),
+    endDate: new Date(2017, 0, 2),
+    expected: {
+      unit: 'day',
+      value: 1,
+      label: 'histogram.gap.day'
+    }
+  }, {
+    startDate: new Date(2017, 0, 1, 1),
+    endDate: new Date(2017, 0, 1, 2),
+    expected: {
+      unit: 'hour',
+      value: 1,
+      label: 'histogram.gap.hour'
+    }
+  }, {
+    startDate: new Date(2017, 0, 1, 1, 1),
+    endDate: new Date(2017, 0, 1, 1, 2),
+    expected: {
+      unit: 'minute',
+      value: 1,
+      label: 'histogram.gap.minute'
+    }
+  }, {
+    startDate: new Date(2017, 0, 1, 1, 1, 1),
+    endDate: new Date(2017, 0, 1, 1, 1, 11),
+    expected: {
+      unit: 'second',
+      value: 10,
+      label: 'histogram.gap.seconds'
+    }
+  }];
+
+  getTimeGapTestCases.forEach((test) => {
+    it(`should the getTimeGap return with the proper time gap obj for 
${test.expected.value} ${test.expected.unit} difference`, () => {
+      const getTimeGap: (startDate: Date, endDate: Date) => {value: number, 
unit: string} = component['getTimeGap'];
+      const gap = getTimeGap(test.startDate, test.endDate);
+      expect(gap).toEqual(test.expected);
+    });
+  });
+
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts
----------------------------------------------------------------------
diff --git 
a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts
 
b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts
index e255166..fb3092f 100644
--- 
a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts
+++ 
b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts
@@ -30,19 +30,19 @@ import {HistogramStyleOptions, HistogramOptions} from 
'@app/classes/histogram-op
 })
 export class TimeHistogramComponent implements OnInit, AfterViewInit, 
OnChanges {
 
-  constructor(private appSettings: AppSettingsService) {
-    appSettings.getParameter('timeZone').subscribe((value: string): void => {
+  constructor(private appSettings: AppSettingsService) {}
+
+  ngOnInit() {
+    this.appSettings.getParameter('timeZone').subscribe((value: string): void 
=> {
       this.timeZone = value;
       this.createHistogram();
     });
-  }
-
-  ngOnInit() {
     this.options = Object.assign({}, this.defaultOptions, this.customOptions);
   }
 
   ngAfterViewInit() {
     this.htmlElement = this.element.nativeElement;
+    this.tooltipElement = this.tooltipEl.nativeElement;
     this.host = d3.select(this.htmlElement);
   }
 
@@ -53,6 +53,9 @@ export class TimeHistogramComponent implements OnInit, 
AfterViewInit, OnChanges
   @ViewChild('container')
   element: ElementRef;
 
+  @ViewChild('tooltipEl')
+  tooltipEl: ElementRef;
+
   @Input()
   svgId: string;
 
@@ -67,15 +70,20 @@ export class TimeHistogramComponent implements OnInit, 
AfterViewInit, OnChanges
 
   private readonly defaultOptions: HistogramStyleOptions = {
     margin: {
-      top: 20,
-      right: 20,
-      bottom: 40,
+      top: 5,
+      right: 50,
+      bottom: 30,
       left: 50
     },
-    height: 200,
+    height: 150,
     tickPadding: 10,
-    columnWidth: 20,
-    dragAreaColor: '#FFF'
+    columnWidth: {
+      second: 40,
+      minute: 30,
+      hour: 25,
+      day: 20,
+      base: 20
+    }
   };
 
   private options: HistogramOptions;
@@ -99,6 +107,7 @@ export class TimeHistogramComponent implements OnInit, 
AfterViewInit, OnChanges
   private yAxis;
 
   private htmlElement: HTMLElement;
+  private tooltipElement: HTMLElement;
 
   private dragArea: Selection<SVGGraphicsElement, undefined, 
SVGGraphicsElement, undefined>;
 
@@ -108,10 +117,68 @@ export class TimeHistogramComponent implements OnInit, 
AfterViewInit, OnChanges
 
   private maxDragX: number;
 
-  private readonly timeFormat: string = 'MM/DD HH:mm';
+  private readonly tickTimeFormat: string = 'MM/DD HH:mm';
+  private readonly historyStartEndTimeFormat = 'dddd, MMMM DD, YYYY';
 
   histogram: any;
 
+  /**
+   * This property is to hold the data of the bar where the mouse is over.
+   */
+  private tooltipInfo: {data: object, timeStamp: number};
+  /**
+   * This is the computed position of the tooltip relative to the @htmlElement 
which is the container of the histogram.
+   * It is set when the mousemoving over the bars in the @handleRectMouseMove 
method.
+   */
+  private tooltipPosition: {top: number, left: number};
+  /**
+   * This property indicates if the tooltip should be positioned on the left 
side of the cursor or not.
+   * It should be true when the tooltip is out from the window.
+   * @type {boolean}
+   */
+  private tooltipOnTheLeft: boolean = false;
+  /**
+   * This property holds the data structure describing the gaps between the 
xAxis ticks.
+   * The unit property can be: second, minute, hour, day
+   * The value is the number of the given unit.
+   */
+  private chartTimeGap: {value: number, unit: string, label: string} | null;
+  /**
+   * This is the rectangle element to represent the unselected time range on 
the left side of the selected time range
+   */
+  private leftDragArea: Selection<SVGGraphicsElement, undefined, 
SVGGraphicsElement, undefined>;
+  /**
+   * This is the rectangle element to represent the unselected time range on 
the right side of the selected time range
+   */
+  private rightDragArea: Selection<SVGGraphicsElement, undefined, 
SVGGraphicsElement, undefined>;
+  /**
+   * This is a Date object holding the value of the first tick of the xAxis. 
It is a helper getter for the template.
+   */
+  private get firstDateTick(): Date | undefined {
+    const ticks = this.xScale && this.xScale.ticks();
+    return (ticks && ticks.length && ticks[0]) || undefined;
+  }
+  /**
+   * This is a Date object holding the value of the last tick of the xAxis. It 
is a helper getter for the template.
+   */
+  private get lastDateTick(): Date | undefined {
+    const ticks = this.xScale && this.xScale.ticks();
+    return (ticks && ticks.length && ticks[ticks.length-1]) || undefined;
+  }
+
+  /**
+   * This will return the information about the used levels and the connected 
colors and labels.
+   * The goal is to provide an easy property to the template to display the 
legend of the levels.
+   * @returns {Array<{level: string; label: string; color: string}>}
+   */
+  private get legends(): Array<{level: string, label: string, color: string}> {
+    return Object.keys(this.options.keysWithColors).map(level => 
Object.assign({},{
+      level,
+      label: `levels.${level.toLowerCase()}`,
+      color: this.options.keysWithColors[level]
+    }));
+  }
+
   private createHistogram(): void {
     if (this.host) {
       this.setup();
@@ -139,13 +206,21 @@ export class TimeHistogramComponent implements OnInit, 
AfterViewInit, OnChanges
       .attr('transform', `translate(${margin.left},${margin.top})`);
   }
 
+  /**
+   * It draws the svg representation of the x axis. The goal is to set the 
ticks here, add the axis to the svg element
+   * and set the position of the axis.
+   */
   private drawXAxis(): void {
     this.xAxis = d3.axisBottom(this.xScale)
-      .tickFormat(tick => 
moment(tick).tz(this.timeZone).format(this.timeFormat))
+      .tickFormat(tick => 
moment(tick).tz(this.timeZone).format(this.tickTimeFormat))
       .tickPadding(this.options.tickPadding);
-    this.svg.append('g').attr('class', 'axis').attr('transform', 
`translate(0,${this.options.height})`).call(this.xAxis);
+    this.svg.append('g').attr('class', 'axis axis-x').attr('transform', 
`translate(0,${this.options.height})`).call(this.xAxis);
   }
 
+  /**
+   * It draws the svg representation of the y axis. The goal is to set the 
ticks here, add the axis to the svg element
+   * and set the position of the axis.
+   */
   private drawYAxis(): void {
     this.yAxis = d3.axisLeft(this.yScale).tickFormat((tick: number): string | 
undefined => {
       if (Number.isInteger(tick)) {
@@ -154,30 +229,248 @@ export class TimeHistogramComponent implements OnInit, 
AfterViewInit, OnChanges
         return;
       }
     }).tickPadding(this.options.tickPadding);
-    this.svg.append('g').attr('class', 'axis').call(this.yAxis).append('text');
+    this.svg.append('g').attr('class', 'axis 
axis-y').call(this.yAxis).append('text');
+  };
+
+  /**
+   * The goal is to handle the mouse over event on the rect svg elements so 
that we can populate the tooltip info object
+   * and set the initial position of the tooltip. So we call the corresponding 
methods.
+   * @param d The data for the currently "selected" bar
+   * @param {number} index The index of the current element in the selection
+   * @param elements The selection of the elements
+   */
+  private handleRectMouseOver = (d: any, index: number, elements: any):void => 
{
+    this.setTooltipDataFromChartData(d);
+    this.setTooltipPosition();
+  };
+
+  /**
+   * The goal is to handle the movement of the mouse over the rect svg 
elements, so that we can set the position of
+   * the tooltip by calling the @setTooltipPosition method.
+   */
+  private handleRectMouseMove = ():void => {
+    this.setTooltipPosition();
+  };
+
+  /**
+   * The goal is to reset the tooltipInfo object so that the tooltip will be 
hidden.
+   */
+  private handleRectMouseOut = ():void => {
+    this.tooltipInfo = null;
+  };
+
+  /**
+   * The goal is set the tooltip
+   * @param d
+   */
+  private setTooltipDataFromChartData(d: {data: any, [key: string]: any}): 
void {
+    let {timeStamp, ...data} = d.data;
+    let levelColors = this.options.keysWithColors;
+    this.tooltipInfo = {
+      data: Object.keys(levelColors).map(key => Object.assign({}, {
+        level: key,
+        levelLabel: `levels.${key.toLowerCase()}`,
+        value: data[key]
+      })),
+      timeStamp
+    };
+  }
+
+  /**
+   * The goal of this function is to set the tooltip position regarding the 
d3.mouse event relative to the @htmlElement.
+   * Onlty if we have @tooltipInfo
+   */
+  private setTooltipPosition():void {
+    if (this.tooltipInfo) {
+      let tEl = this.tooltipElement;
+      let pos = d3.mouse(this.htmlElement);
+      let left = pos[0];
+      let top = pos[1] - (tEl.offsetHeight / 2);
+      let tooltipWidth = tEl.offsetWidth;
+      let windowSize = window.innerWidth;
+      if (left + tooltipWidth > windowSize) {
+        left = pos[0] - (tooltipWidth + 25);
+      }
+      this.tooltipOnTheLeft = left < pos[0];
+      this.tooltipPosition = {left, top};
+    }
+  };
+
+  /**
+   * The goal is to calculate the time gap between the given dates. It will 
return an object representing the unit and
+   * the value in the given unit. Eg.: {unit: 'minute', value: 5}
+   * @param {Date} startDate
+   * @param {Date} endDate
+   * @returns {{value: number; unit: string, label: string}}
+   */
+  private getTimeGap(startDate: Date, endDate: Date): {value: number, unit: 
string, label: string} {
+    const startDateMoment = moment(startDate);
+    const endDateMoment = moment(endDate);
+    const diffInWeek: number = endDateMoment.diff(startDateMoment, 'weeks');
+    const diffInDay: number = endDateMoment.diff(startDateMoment, 'days');
+    const diffInHour: number = endDateMoment.diff(startDateMoment, 'hours');
+    const diffInMin: number = endDateMoment.diff(startDateMoment, 'minutes');
+    const diffInSec: number = endDateMoment.diff(startDateMoment, 'seconds');
+    const value = diffInWeek >= 1 ? diffInWeek : (
+      diffInDay >= 1 ? diffInDay : (
+        diffInHour >= 1 ? diffInHour : (diffInMin >= 1 ? diffInMin : diffInSec)
+      )
+    );
+    const unit: string = diffInWeek >= 1 ? 'week' : (
+      diffInDay >= 1 ? `day` : (
+        diffInHour >= 1 ? `hour` : (diffInMin >= 1 ? `minute` : `second`)
+      )
+    );
+    const label = `histogram.gap.${unit}${value>1 ? 's' : ''}`;
+    return {
+      value,
+      unit,
+      label
+    };
+  }
+
+  /**
+   * The goal is to have a simple function to set the time gap corresponding 
to the xScale ticks.
+   * It will reset the time gap if the xScale is not set or there are no ticks.
+   */
+  private setChartTimeGapByXScale() {
+    let ticks = this.xScale && this.xScale.ticks();
+    if (ticks && ticks.length) {
+      this.setChartTimeGap(ticks[0], ticks[1] || ticks[0]);
+    } else {
+      this.resetChartTimeGap();
+    }
+  }
+
+  /**
+   * Simply reset the time gap property to null.
+   */
+  private resetChartTimeGap(): void {
+    this.chartTimeGap = null;
+  }
+
+  /**
+   * The goal is to have a single point where we set the chartTimeGap property 
corresponding the given timerange.
+   * @param {Date} startDate
+   * @param {Date} endDate
+   */
+  private setChartTimeGap(startDate: Date, endDate: Date): void {
+    this.chartTimeGap = this.getTimeGap(startDate, endDate);
+  }
+
+  /**
+   * Set the domain for the y scale regarding the given data. The maximum 
value of the data is the sum of the log level
+   * values.
+   * An example data: [{timeStamp: 1233455677, WARN: 12, ERROR: 123}]
+   * @param {Array<{timeStamp: number; [p: string]: number}>} data
+   */
+  private setYScaleDomain(data: Array<{timeStamp: number, [key: string]: 
number}>): void {
+    const keys = Object.keys(this.options.keysWithColors);
+    const maxYValue = d3.max(data, item => keys.reduce((sum: number, key: 
string): number => sum + item[key], 0));
+    this.yScale.domain([0, maxYValue]);
+  }
+
+  /**
+   * Set the domain values for the x scale regarding the given data.
+   * An example data: [{timeStamp: 1233455677, WARN: 12, ERROR: 123}]
+   * @param {Array<{timeStamp: number; [p: string]: any}>} data
+   */
+  private setXScaleDomain(data: Array<{timeStamp: number, [key: string]: 
any}>): void {
+    this.xScale.domain(d3.extent(data, item => item.timeStamp)).nice();
   }
 
   private populate(): void {
-    const keys = Object.keys(this.options.keysWithColors),
-      data = this.data,
-      timeStamps = Object.keys(data),
-      formattedData = timeStamps.map((timeStamp: string): {[key: string]: 
number} => Object.assign({
+    const keys = Object.keys(this.options.keysWithColors);
+    const data = this.data;
+    const timeStamps = Object.keys(data);
+    // we create a more consumable data structure for d3
+    const formattedData = timeStamps.map((timeStamp: string): {timeStamp: 
number, [key: string]: number} => Object.assign({
         timeStamp: Number(timeStamp)
-      }, data[timeStamp])),
-      layers = (d3.stack().keys(keys)(formattedData)),
-      columnWidth = this.options.columnWidth;
-    this.xScale.domain(d3.extent(formattedData, item => item.timeStamp));
-    this.yScale.domain([0, d3.max(formattedData, item => keys.reduce((sum: 
number, key: string): number => sum + item[key], 0))]);
+      }, data[timeStamp]));
+    const layers = (d3.stack().keys(keys)(formattedData));
+
+    // after we have the data we set the domain values both scales
+    this.setXScaleDomain(formattedData);
+    this.setYScaleDomain(formattedData);
+
+    // Setting the timegap label above the chart
+    this.setChartTimeGapByXScale();
+
+    let unitD3TimeProp = this.chartTimeGap.unit.charAt(0).toUpperCase() + 
this.chartTimeGap.unit.slice(1);
+    this.xScale.nice(d3[`time${unitD3TimeProp}`], 2);
+
+    let columnWidth = this.options.columnWidth[this.chartTimeGap.unit] || 
this.options.columnWidth.base;
+
+    // drawing the axis
     this.drawXAxis();
     this.drawYAxis();
-    const layer = 
this.svg.selectAll().data(d3.transpose<any>(layers)).enter().append('g').attr('class',
 'value');
-    layer.selectAll().data(item => item).enter().append('rect')
-      .attr('x', item => this.xScale(item.data.timeStamp) - columnWidth / 
2).attr('y', item => this.yScale(item[1]))
-      .attr('height', item => this.yScale(item[0]) - 
this.yScale(item[1])).attr('width', columnWidth.toString())
-      .style('fill', (item, index) => this.colorScale(index));
+
+    // populate the data and drawing the bars
+    const layer = this.svg.selectAll('.value').data(d3.transpose<any>(layers))
+                    .attr('class', 'value')
+                  .enter().append('g')
+                    .attr('class', 'value');
+    layer.selectAll('.value rect').data(item => item)
+        .attr('x', item => this.xScale(item.data.timeStamp) - columnWidth / 2)
+        .attr('y', item => this.yScale(item[1]))
+        .attr('height', item => this.yScale(item[0]) - this.yScale(item[1]))
+        .attr('width', columnWidth.toString())
+        .style('fill', (item, index) => this.colorScale(index))
+      .enter().append('rect')
+        .attr('x', item => this.xScale(item.data.timeStamp) - columnWidth / 2)
+        .attr('y', item => this.yScale(item[1]))
+        .attr('height', item => this.yScale(item[0]) - this.yScale(item[1]))
+        .attr('width', columnWidth.toString())
+        .style('fill', (item, index) => this.colorScale(index))
+        .on('mouseover', this.handleRectMouseOver)
+        .on('mousemove', this.handleRectMouseMove)
+        .on('mouseout', this.handleRectMouseOut);
     this.setDragBehavior();
   }
 
+  private getTimeRangeByXRanges(startX: number, endX:number): [number, number] 
{
+    const xScaleInterval = this.xScale.domain().map((point: Date): number => 
point.valueOf());
+    const xScaleLength = xScaleInterval[1] - xScaleInterval[0];
+    const ratio = xScaleLength / this.width;
+    return [Math.round(xScaleInterval[0] + ratio * startX), 
Math.round(xScaleInterval[0] + ratio * endX)];
+  }
+
+  /**
+   * The goal is to create the two shadow rectangle beside the selected area. 
Actually we blurout the not selected
+   * timeranges
+   * @param {number} startX This is the starting position of the drag event 
withing the container
+   * @param {number} currentX This is the ending point of the drag within the 
container
+   */
+  private createInvertDragArea(startX: number, currentX: number): void {
+    const height: number = this.options.height + this.options.margin.top + 
this.options.margin.bottom;
+    this.leftDragArea = this.svg.insert('rect').attr('height', 
height).attr('class', 'unselected-drag-area');
+    this.rightDragArea = this.svg.insert('rect').attr('height', 
height).attr('class', 'unselected-drag-area');
+    this.setInvertDragArea(startX, currentX);
+  }
+
+  /**
+   * Set the position and the width of the blur/shadow rectangles of the 
unselected area(s).
+   * @param {number} startX The start point of the selected area.
+   * @param {number} currentX The end point of the selected area.
+   */
+  private setInvertDragArea(startX: number, currentX: number): void {
+    const left: number = Math.min(startX, currentX);
+    const right: number = Math.max(startX, currentX);
+    let rightAreaWidth: number = this.width - right;
+    rightAreaWidth = rightAreaWidth > 0 ? rightAreaWidth : 0;
+    let leftAreaWidth: number = left > 0 ? left : 0;
+    this.leftDragArea.attr('x', 0).attr('width', leftAreaWidth);
+    this.rightDragArea.attr('x', right).attr('width', rightAreaWidth);
+  }
+
+  /**
+   * The goal is to have a single point where we remove the rectangles of the 
blur/shadow, unselected time range(s)
+   */
+  private clearInvertDragArea(): void {
+    this.leftDragArea.remove();
+    this.rightDragArea.remove();
+  }
+
   private setDragBehavior(): void {
     this.minDragX = this.options.margin.left;
     this.maxDragX = this.htmlElement.clientWidth;
@@ -188,25 +481,25 @@ export class TimeHistogramComponent implements OnInit, 
AfterViewInit, OnChanges
         }
         this.dragStartX = Math.max(0, this.getDragX(containers[0]) - 
this.options.margin.left);
         this.dragArea = this.svg.insert('rect', ':first-child').attr('x', 
this.dragStartX).attr('y', 0).attr('width', 0)
-          .attr('height', this.options.height).style('fill', 
this.options.dragAreaColor);
+          .attr('height', this.options.height).attr('class', 'drag-area');
       })
       .on('drag', (datum: undefined, index: number, containers: 
ContainerElement[]): void => {
-        const currentX = Math.max(this.getDragX(containers[0]), this.minDragX) 
- this.options.margin.left,
-          startX = Math.min(currentX, this.dragStartX),
-          currentWidth = Math.abs(currentX - this.dragStartX);
+        const mousePos = this.getDragX(containers[0]);
+        const currentX = Math.max(mousePos, this.minDragX) - 
this.options.margin.left;
+        const startX = Math.min(currentX, this.dragStartX);
+        const currentWidth = Math.abs(currentX - this.dragStartX);
         this.dragArea.attr('x', startX).attr('width', currentWidth);
+        let timeRange = this.getTimeRangeByXRanges(startX, startX + 
currentWidth);
+        this.setChartTimeGap(new Date(timeRange[0]), new Date(timeRange[1]));
       })
       .on('end', (): void => {
-        const dragAreaDetails = this.dragArea.node().getBBox(),
-          startX = Math.max(0, dragAreaDetails.x),
-          endX = Math.min(this.width, dragAreaDetails.x + 
dragAreaDetails.width),
-          xScaleInterval = this.xScale.domain().map((point: Date): number => 
point.valueOf()),
-          xScaleLength = xScaleInterval[1] - xScaleInterval[0],
-          ratio = xScaleLength / this.width,
-          startTimeStamp = Math.round(xScaleInterval[0] + ratio * startX),
-          endTimeStamp = Math.round(xScaleInterval[0] + ratio * endX);
-        this.selectArea.emit([startTimeStamp, endTimeStamp]);
+        const dragAreaDetails = this.dragArea.node().getBBox();
+        const startX = Math.max(0, dragAreaDetails.x);
+        const endX = Math.min(this.width, dragAreaDetails.x + 
dragAreaDetails.width);
+        const dateRange: [number, number] = this.getTimeRangeByXRanges(startX, 
endX);
+        this.selectArea.emit(dateRange);
         this.dragArea.remove();
+        this.setChartTimeGap(new Date(dateRange[0]), new Date(dateRange[1]));
       })
     );
     d3.selectAll(`svg#${this.svgId} .value, svg#${this.svgId} 
.axis`).call(d3.drag().on('start', (): void => {

http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
----------------------------------------------------------------------
diff --git 
a/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
 
b/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
index e754aa4..d719893 100644
--- 
a/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
+++ 
b/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
@@ -476,9 +476,9 @@ export class LogsContainerService {
   };
 
   readonly colors = {
-    WARN: '#FF8916',
-    ERROR: '#E81D1D',
     FATAL: '#830A0A',
+    ERROR: '#E81D1D',
+    WARN: '#FF8916',
     INFO: '#2577B5',
     DEBUG: '#65E8FF',
     TRACE: '#888',

http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json 
b/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
index 6c916aa..3f4f5c8 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
+++ b/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
@@ -158,5 +158,16 @@
   "logs.totalEventFound": "{{totalCount}} events found",
   "logs.noEventFound": "No event found",
   "logs.hideGraph": "Hide Graph",
-  "logs.showGraph": "Show Graph"
+  "logs.showGraph": "Show Graph",
+
+  "histogram.gap": "gap",
+  "histogram.gaps": "gaps",
+  "histogram.gap.second": "second",
+  "histogram.gap.seconds": "seconds",
+  "histogram.gap.minute": "minute",
+  "histogram.gap.minutes": "minutes",
+  "histogram.gap.hour": "hour",
+  "histogram.gap.hours": "hours",
+  "histogram.gap.day": "day",
+  "histogram.gap.days": "days"
 }

Reply via email to