Skip to content
Commits on Source (2)
......@@ -113,6 +113,9 @@ import { MarketingComponent } from './components/marketing/marketing.component';
import { MarketingFooterComponent } from './components/marketing/footer.component';
import { ToggleComponent } from './components/toggle/toggle.component';
import { MarketingAsFeaturedInComponent } from './components/marketing/as-featured-in.component';
import { SidebarMenuComponent } from './components/sidebar-menu/sidebar-menu.component';
import { ChartV2Component } from './components/chart-v2/chart-v2.component';
import { MiniChartComponent } from './components/mini-chart/mini-chart.component';
@NgModule({
imports: [
......@@ -215,6 +218,9 @@ import { MarketingAsFeaturedInComponent } from './components/marketing/as-featur
MarketingComponent,
MarketingFooterComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
MiniChartComponent,
],
exports: [
MINDS_PIPES,
......@@ -305,6 +311,7 @@ import { MarketingAsFeaturedInComponent } from './components/marketing/as-featur
ToggleComponent,
MarketingComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
],
providers: [
SiteService,
......
<!-- <div
#chartContainer
class="m-chartV2__chartContainer"
[ngClass]="{ isTouchDevice: isTouchDevice }"
>
<plotly-plot
*ngIf="init"
#graphDiv
id="graphDiv"
[data]="data"
[layout]="layout"
[config]="config"
[useResizeHandler]="true"
[style]="{ position: 'relative' }"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
(plotly_click)="onClick($event)"
>
</plotly-plot>
</div>
<div #hoverInfoDiv id="hoverInfoDiv" class="m-chartV2__hoverInfoDiv">
<i *ngIf="isTouchDevice" class="material-icons" (click)="onUnhover($event)"
>close</i
>
<div class="m-chartV2__hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div
[ngSwitch]="selectedMetric?.unit"
class="m-chartV2__hoverInfo__row--primary"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number }} {{ selectedMetric.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ selectedMetric?.unit }}
</ng-template>
</div>
<div class="m-chartV2__hoverInfo__row" *ngIf="isComparison">
vs
<ng-container
[ngSwitch]="selectedMetric?.unit"
class="m-chartV2__hoverInfo__row"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }}
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
</div>
</div> -->
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsMenuComponent } from './menu.component';
import { ChartV2Component } from './chart-v2.component';
describe('AnalyticsMenuComponent', () => {
let component: AnalyticsMenuComponent;
let fixture: ComponentFixture<AnalyticsMenuComponent>;
describe('ChartV2Component', () => {
let component: ChartV2Component;
let fixture: ComponentFixture<ChartV2Component>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsMenuComponent],
declarations: [ChartV2Component],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsMenuComponent);
fixture = TestBed.createComponent(ChartV2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
OnDestroy,
HostListener,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
ViewChild,
ElementRef,
} from '@angular/core';
// import { Subscription } from 'rxjs';
// import { map } from 'rxjs/operators';
// import {
// AnalyticsDashboardService,
// Timespan as TimespanBase,
// Buckets,
// } from '../../dashboard.service';
// import * as Plotly from 'plotly.js';
// import chartPalette from '../../chart-palette.default';
// import { ThemeService } from '../../../../../common/services/theme.service';
// import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
@Component({
selector: 'm-chartV2',
templateUrl: './chart-v2.component.html',
})
export class ChartV2Component implements OnInit {
constructor() {}
ngOnInit() {}
}
// -------------------------------------------------------------------------
// interface TimespanExtended extends TimespanBase {
// xTickFormat?: string;
// datePipe?: string;
// }
// export { TimespanExtended as Timespan };
// @Component({
// selector: 'm-analytics__chart',
// templateUrl: 'chart.component.html',
// changeDetection: ChangeDetectionStrategy.OnPush,
// })
// export class AnalyticsChartComponent implements OnDestroy, OnInit {
// @ViewChild('graphDiv', { static: true }) graphDiv;
// @ViewChild('hoverInfoDiv', { static: true }) hoverInfoDivEl: ElementRef;
// @ViewChild('chartContainer', { static: true }) chartContainer: ElementRef;
// isTouchDevice: boolean;
// init: boolean = false;
// hoverInfoDiv: any;
// hoverInfo: any = {};
// metricSubscription: Subscription;
// selectedMetric$ = this.analyticsService.metrics$.pipe(
// map(metrics => {
// return metrics.find(metric => metric.visualisation !== null);
// })
// );
// selectedMetric;
// timespansSubscription: Subscription;
// selectedTimespan;
// themeSubscription: Subscription;
// isDark: boolean = false;
// segments: Buckets[];
// isComparison: boolean = false;
// data = [];
// layout;
// config = {
// displayModeBar: false,
// // responsive: true,
// };
// pointsPerSegment = 1;
// hoverPoint: number;
// lineRange: Array<any>;
// newLineRange = true;
// markerFills;
// shapes = [];
// timespanFormats = [
// { interval: 'day', xTickFormat: '%m/%d', datePipe: 'EEE MMM d, y' },
// { interval: 'month', xTickFormat: '%m/%Y', datePipe: 'MMM y' },
// ];
// datePipe: string = this.timespanFormats[0].datePipe;
// xTickFormat: string = this.timespanFormats[0].xTickFormat;
// // yTickPrefix: string = '';
// yTickFormat: string = '';
// // ***********************************************************
// constructor(
// private analyticsService: AnalyticsDashboardService,
// private themeService: ThemeService,
// protected cd: ChangeDetectorRef
// ) {}
// ngOnInit() {
// this.isTouchDevice = isMobileOrTablet();
// this.hoverInfoDiv = this.hoverInfoDivEl.nativeElement;
// this.timespansSubscription = this.analyticsService.timespans$.subscribe(
// timespans => {
// this.selectedTimespan = timespans.find(
// timespan => timespan.selected === true
// );
// const timespanFormat =
// this.timespanFormats.find(
// t => t.interval === this.selectedTimespan.interval
// ) || this.timespanFormats[0];
// this.xTickFormat = timespanFormat.xTickFormat;
// this.datePipe = timespanFormat.datePipe;
// if (this.init) {
// this.layout.xaxis.tickformat = this.xTickFormat;
// }
// this.detectChanges();
// }
// );
// this.themeSubscription = this.themeService.isDark$.subscribe(isDark => {
// this.isDark = isDark;
// if (this.init) {
// this.getData();
// this.getLayout();
// }
// this.detectChanges();
// });
// this.metricSubscription = this.selectedMetric$.subscribe(metric => {
// this.init = false;
// this.selectedMetric = metric;
// if (metric.unit && metric.unit === 'usd') {
// this.yTickFormat = '$.2f';
// }
// try {
// this.initPlot();
// } catch (err) {
// console.log(err);
// }
// this.detectChanges();
// });
// }
// swapSegmentColors() {
// const tempPaletteItem = chartPalette.segmentColorIds[0];
// chartPalette.segmentColorIds[0] = chartPalette.segmentColorIds[1];
// chartPalette.segmentColorIds[1] = tempPaletteItem;
// }
// initPlot() {
// this.segments = this.selectedMetric.visualisation.segments;
// if (this.segments.length === 2) {
// this.isComparison = true;
// // Reverse the segments so comparison line is layered behind current line
// this.segments.reverse();
// console.log(this.segments);
// // Current line should be blue
// this.swapSegmentColors();
// }
// this.pointsPerSegment = this.segments[0].buckets.length;
// for (let i = 0; i < this.pointsPerSegment; i++) {
// this.shapes[i] = {
// type: 'line',
// layer: 'below',
// x0: this.segments[0].buckets[i].date,
// y0: 0,
// x1: this.segments[0].buckets[i].date,
// y1: 0,
// line: {
// color: 'rgba(0, 0, 0, 0)',
// width: 2,
// },
// };
// }
// this.getData();
// this.getLayout();
// this.init = true;
// this.detectChanges();
// }
// getData() {
// this.markerFills = [];
// this.segments.forEach((segment, index) => {
// const segmentMarkerFills = [];
// for (let i = 0; i < this.pointsPerSegment; i++) {
// segmentMarkerFills[i] = this.getColor('m-white');
// }
// this.markerFills.push(segmentMarkerFills);
// });
// const globalSegmentSettings = {
// type: 'scatter',
// mode: 'lines+markers',
// line: {
// width: 1,
// dash: 'solid',
// },
// marker: {
// size: 7,
// },
// showlegend: false,
// hoverinfo: 'text',
// x: this.unpack(this.segments[0].buckets, 'date'),
// };
// this.segments.forEach((s, i) => {
// const segment = {
// ...globalSegmentSettings,
// line: {
// ...globalSegmentSettings.line,
// color: this.getColor(chartPalette.segmentColorIds[i]),
// },
// marker: {
// ...globalSegmentSettings.marker,
// color: this.markerFills[i],
// line: {
// color: this.getColor(chartPalette.segmentColorIds[i]),
// width: 1,
// },
// },
// y: this.unpack(this.segments[i].buckets, 'value'),
// };
// this.data[i] = segment;
// });
// if (this.isComparison) {
// this.data[0].line.dash = 'dot';
// }
// }
// getLayout() {
// this.layout = {
// width: 0,
// height: 0,
// autoexpand: 'true',
// autosize: 'true',
// hovermode: 'x',
// paper_bgcolor: this.getColor('m-white'),
// plot_bgcolor: this.getColor('m-white'),
// font: {
// family: 'Roboto',
// },
// xaxis: {
// tickformat: this.xTickFormat,
// tickmode: 'array',
// tickson: 'labels',
// tickcolor: this.getColor('m-grey-130'),
// tickangle: -45,
// tickfont: {
// color: this.getColor('m-grey-130'),
// },
// showgrid: false,
// showline: true,
// linecolor: this.getColor('m-grey-300'),
// linewidth: 1,
// zeroline: false,
// fixedrange: true,
// // automargin: true,
// // rangemode: 'nonnegative',
// },
// yaxis: {
// ticks: '',
// tickformat: this.yTickFormat,
// tickmode: 'array',
// tickson: 'labels',
// showgrid: true,
// gridcolor: this.getColor('m-grey-70'),
// zeroline: false,
// visible: true,
// side: 'right',
// tickfont: {
// color: this.getColor('m-grey-130'),
// },
// fixedrange: true,
// // automargin: true,
// // rangemode: 'nonnegative',
// },
// margin: {
// t: 16,
// b: 80,
// l: 0,
// r: 80,
// pad: 16,
// },
// shapes: this.shapes,
// };
// }
// onHover($event) {
// console.log($event);
// this.hoverPoint = $event.points[0].pointIndex;
// this.addMarkerFill();
// this.showShape($event);
// this.positionHoverInfo($event);
// this.populateHoverInfo();
// this.hoverInfoDiv.style.opacity = 1;
// this.detectChanges();
// }
// onUnhover($event) {
// this.emptyMarkerFill();
// this.hideShape();
// this.hoverInfoDiv.style.opacity = 0;
// this.detectChanges();
// }
// onClick($event) {
// if (!this.isTouchDevice) {
// return;
// }
// // TODO: use this for non-hover devices
// }
// addMarkerFill() {
// this.data.forEach((segment, i) => {
// this.markerFills[i][this.hoverPoint] = this.getColor(
// chartPalette.segmentColorIds[i]
// );
// });
// }
// emptyMarkerFill() {
// this.data.forEach((segment, i) => {
// this.markerFills[i][this.hoverPoint] = this.getColor('m-white');
// segment.marker.color = this.markerFills[i];
// });
// }
// showShape($event) {
// const hoverLine = this.shapes[this.hoverPoint];
// // Without this, entire graph resizes on every hover
// if (this.newLineRange) {
// this.newLineRange = false;
// this.lineRange = $event.yaxes[0].range;
// }
// hoverLine.y0 = this.lineRange[0];
// hoverLine.y1 = this.lineRange[1] * 0.99;
// hoverLine.line.color = this.getColor('m-grey-70');
// this.layout.shapes = this.shapes;
// }
// hideShape() {
// this.shapes[this.hoverPoint].line.color = 'rgba(0, 0, 0, 0)';
// this.layout.shapes = this.shapes;
// }
// populateHoverInfo() {
// const pt = this.isComparison ? 1 : 0;
// // TODO: format value strings here and remove ngSwitch from template?
// this.hoverInfo['date'] = this.segments[pt].buckets[this.hoverPoint].date;
// this.hoverInfo['value'] =
// this.selectedMetric.unit !== 'usd'
// ? this.segments[pt].buckets[this.hoverPoint].value
// : this.segments[pt].buckets[this.hoverPoint].value / 100;
// if (this.isComparison && this.segments[1]) {
// this.hoverInfo['comparisonValue'] =
// this.selectedMetric.unit !== 'usd'
// ? this.segments[0].buckets[this.hoverPoint].value
// : this.segments[0].buckets[this.hoverPoint].value / 100;
// this.hoverInfo['comparisonDate'] = this.segments[0].buckets[
// this.hoverPoint
// ].date;
// }
// }
// positionHoverInfo($event) {
// const pad = 16,
// pt = this.isComparison ? 1 : 0,
// xAxis = $event.points[pt].xaxis,
// yAxis = $event.points[pt].yaxis,
// pointXDist = xAxis.d2p($event.points[pt].x) + xAxis._offset,
// pointYDist = yAxis.d2p($event.points[pt].y) + yAxis._offset,
// plotRect = document
// .querySelector('.js-plotly-plot')
// .getBoundingClientRect(),
// hoverInfoRect = this.hoverInfoDiv.getBoundingClientRect();
// if (pointYDist < plotRect.height / 2) {
// // If point is in top half of plot, hoverinfo should go beneath it
// this.hoverInfoDiv.style.top = pointYDist + pad + 'px';
// } else {
// this.hoverInfoDiv.style.top =
// pointYDist - pad - hoverInfoRect.height + 'px';
// }
// if (pointXDist < plotRect.width / 2) {
// // If point is in left half of plot, hoverinfo should go on the right
// this.hoverInfoDiv.style.left = pointXDist + pad + 'px';
// } else {
// this.hoverInfoDiv.style.left =
// pointXDist - pad - hoverInfoRect.width + 'px';
// }
// }
// @HostListener('window:resize')
// applyDimensions() {
// if (this.init) {
// console.log(
// 'chartcontainer: W ' +
// this.chartContainer.nativeElement.clientWidth +
// ', H ' +
// this.chartContainer.nativeElement.clientHeight
// );
// this.layout.width = this.chartContainer.nativeElement.clientWidth - 56; //- 32;
// this.layout.height = this.chartContainer.nativeElement.clientHeight;
// // this.layout = {
// // ...this.layout,
// // width: this.chartContainer.nativeElement.clientWidth, // - 32,
// // height: this.chartContainer.nativeElement.clientHeight, // - 32,
// // };
// this.newLineRange = true;
// this.detectChanges();
// }
// }
// // * UTILITY -----------------------------------
// unpack(rows, key) {
// return rows.map(row => {
// if (key === 'date') {
// return row[key].slice(0, 10);
// } else if (this.selectedMetric.unit === 'usd') {
// return row[key] / 100;
// } else {
// return row[key];
// }
// });
// }
// getColor(colorId) {
// const palette = chartPalette.themeMaps;
// let colorCode = '#607d8b';
// if (palette.find(color => color.id === colorId)) {
// colorCode = palette.find(color => color.id === colorId).themeMap[
// +this.isDark
// ];
// }
// return colorCode;
// }
// detectChanges() {
// this.cd.markForCheck();
// this.cd.detectChanges();
// }
// }
/////////////////////////////////////////////////////////////////////
// @Component({
// selector: 'm-graph',
// template: `
// <plotly-plot
// [data]="data"
// [layout]="_layout"
// [config]="_config"
// ></plotly-plot>
// `,
// })
// export class Graph {
// @Input() data;
// @Input() layout;
// @Input() config;
// get _config() {
// return {
// ...this.config,
// ...{
// displayModeBar: false,
// },
// };
// }
// get _layout() {
// return {
// ...this.layout,
// /*...{
// margin: {
// t: 0,
// b: 0,
// },
// },*/
// };
// }
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsLayoutTableComponent } from './layout-table.component';
import { MiniChartComponent } from './mini-chart.component';
describe('AnalyticsLayoutTableComponent', () => {
let component: AnalyticsLayoutTableComponent;
let fixture: ComponentFixture<AnalyticsLayoutTableComponent>;
describe('MiniChartComponent', () => {
let component: MiniChartComponent;
let fixture: ComponentFixture<MiniChartComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsLayoutTableComponent],
declarations: [MiniChartComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsLayoutTableComponent);
fixture = TestBed.createComponent(MiniChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'm-analytics__layout--table',
templateUrl: './layout-table.component.html',
selector: 'm-miniChart',
templateUrl: './mini-chart.component.html',
})
export class AnalyticsLayoutTableComponent implements OnInit {
export class MiniChartComponent implements OnInit {
constructor() {}
ngOnInit() {}
......
const sidebarMenuCategories = [
{
category: {
id: 'analytics',
label: 'Analytics',
path: '/analytics/dashboard/',
permissions: ['admin', 'user'],
},
subcategories: [
// {
// id: 'summary',
// label: 'Summary',
// permissions: ['admin', 'user'],
// },
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
},
// {
// id: 'engagement',
// label: 'Engagement',
// permissions: ['admin', 'user'],
// },
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// },
],
},
// {
// category: {
// id: 'test1',
// label: 'Test1',
// permissions: ['admin', 'user'],
// path: '/somepath/bork',
// },
// subcategories: [
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// },
// {
// id: 'nodes2',
// label: 'Nodes2',
// permissions: ['admin'],
// },
// ],
// },
// {
// category: {
// id: 'test2',
// label: 'Test2 no subcats',
// path: '/anotherpath/test2',
// },
// },
];
export default sidebarMenuCategories;
<section class="m-sidebarMenu">
<div class="m-sidebarMenu__topbar">
<i class="material-icons" (click)="mobileMenuExpanded = true">menu</i>
<div class="m-sidebarMenu__topbarCatLabel" *ngIf="activeCat">
{{ activeCat.category.label }}
</div>
</div>
<div
class="m-sidebarMenu__overlay"
[ngClass]="{ mobileMenuExpanded: mobileMenuExpanded }"
(click)="mobileMenuExpanded = false"
></div>
<div
class="m-sidebarMenu__sidebar"
[ngClass]="{ mobileMenuExpanded: mobileMenuExpanded }"
>
<a class="m-sidebarMenu__userWrapper" [routerLink]="['/', user.username]">
<img
class="m-sidebarMenu__userAvatar"
[src]="minds.cdn_url + 'icon/' + user.guid + '/small/' + user.icontime"
/>
<div class="m-sidebarMenu__userDetails">
<div class="m-sidebarMenu__userDetails__name">{{ user.name }}</div>
<div class="m-sidebarMenu__userDetails__username">
@{{ user.username }}
</div>
<!-- TODO: get subscriberCount and remove username -->
<!-- <div class="m-sidebarMenu__userDetails__subscribers">
{{ user.subscribers_count | abbr }} subscribers
</div> -->
</div>
</a>
<ng-container *ngFor="let cat of cats">
<div
class="m-sidebarMenu__catContainer"
*ngIf="cat.category.permissionGranted"
[ngClass]="{ expanded: cat.category.expanded }"
>
<div class="m-sidebarMenu__catLabel">
<h3>{{ cat.category.label }}</h3>
<i
class="material-icons"
*ngIf="cat.category.expanded && cat.subcategories"
(click)="cat.category.expanded = false"
>keyboard_arrow_up</i
>
<i
class="material-icons"
*ngIf="!cat.category.expanded && cat.subcategories"
(click)="cat.category.expanded = true"
>keyboard_arrow_down</i
>
</div>
<div class="m-sidebarMenu__subcatContainer" *ngIf="cat.subcategories">
<div
class="m-sidebarMenu__subcat"
*ngFor="let subcat of cat.subcategories"
>
<a
*ngIf="subcat.permissionGranted"
class="m-sidebarMenu__subcatLabel"
(click)="mobileMenuExpanded = false"
[routerLink]="'../' + subcat.id"
routerLinkActive="selected"
>{{ subcat.label }}</a
>
</div>
</div>
</div>
</ng-container>
</div>
</section>
// .m-sidebarMarkers__container,
// m-v2-topbar {
// display: none;
// }
m-sidebarMenu {
display: block;
// min-width: 180px;
// padding: 16px 16px 16px 80px;
// flex: 1 1 0px;
.m-sidebarMenu {
padding: 16px 16px 16px 80px;
}
i {
display: none;
cursor: pointer;
}
.m-sidebarMenu__topbar,
.m-sidebarMenu__userWrapper {
display: none;
}
.m-sidebarMenu__catContainer {
.m-sidebarMenu__subcatContainer {
display: block;
cursor: pointer;
}
}
// .m-sidebarMenu__sidebar {
// position: relative;
// position: -webkit-sticky;
// position: sticky;
// top: 0;
// }
.page.isMobile m-analytics__menu {
margin-right: -32px;
flex: 0 1 0px;
}
.m-sidebarMenu__catLabel {
display: flex;
justify-content: space-between;
align-items: center;
i {
display: none;
}
}
m-analytics__menu {
display: block;
max-width: 160px;
// ----------------------------------------
// MOBILE
.m-sidebarMenu__subcatContainer {
cursor: pointer;
display: none;
.m-sidebarMenu__subcat {
a {
display: block;
padding: 8px 0;
text-decoration: none;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-300);
}
}
a.selected,
&:hover a {
@include m-theme() {
color: themed($m-blue);
}
}
}
}
}
// --------------------------------------------------
// TABLET & MOBILE
// --------------------------------------------------
@media screen and (max-width: $min-tablet) {
m-sidebarMenu {
margin-right: -32px;
flex: 0 1 0px;
padding: 0;
.m-sidebarMenu {
padding: 0 8px;
}
.isMobile {
.topbar {
.m-sidebarMenu__topbar {
display: block;
z-index: 99999;
position: fixed;
top: 0;
......@@ -24,7 +80,7 @@ m-analytics__menu {
padding: 16px;
text-align: center;
@include m-theme() {
background-color: themed($m-grey-100);
background-color: themed($m-grey-50);
color: themed($m-grey-800);
}
......@@ -35,34 +91,32 @@ m-analytics__menu {
left: 16px;
transform: translateY(-50%);
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-700);
color: themed($m-grey-300);
}
}
.pageTitle {
.m-sidebarMenu__topbarCatLabel {
font-size: 20px;
margin: 0;
margin: 0 0 0 -24px;
min-height: 20px;
}
}
.overlay {
.m-sidebarMenu__overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: -1;
// display: none;
background-color: transparent;
transition: background-color 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
&.expanded {
// display: block;
&.mobileMenuExpanded {
z-index: 999998;
@include m-theme() {
background-color: rgba(themed($m-grey-700), 0.2);
}
}
}
.sidebar {
.m-sidebarMenu__sidebar {
z-index: 999999;
position: fixed;
top: 0;
......@@ -76,50 +130,66 @@ m-analytics__menu {
@include m-theme() {
background-color: themed($m-white);
}
&.expanded {
&.mobileMenuExpanded {
left: 0;
}
.sidebarTitle {
.m-sidebarMenu__catContainer {
.m-sidebarMenu__subcatContainer {
display: none;
}
&.expanded {
.m-sidebarMenu__subcatContainer {
display: block;
.m-sidebarMenu__subcat {
a {
padding: 6px 0;
}
}
}
}
}
.m-sidebarMenu__catLabel {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
font-size: 20px;
margin: 0;
margin: 16px 0;
}
i {
font-size: 20px;
display: inline-block;
font-size: 26px;
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.profile {
.m-sidebarMenu__userWrapper {
display: flex;
text-decoration: none;
margin: 24px 0;
@include m-theme() {
color: themed($m-grey-800);
}
.avatar {
.m-sidebarMenu__userAvatar {
border-radius: 50%;
margin-right: 16px;
}
.details {
.m-sidebarMenu__userDetails {
& > {
padding: 8px 0;
}
.name {
.m-sidebarMenu__userDetails__name {
font-weight: bold;
}
.username {
.m-sidebarMenu__userDetails__username {
@include m-theme() {
color: themed($m-grey-200);
}
}
.subscribers {
.m-sidebarMenu__userDetails__subscribers {
font-size: 11px;
@include m-theme() {
color: themed($m-grey-200);
......@@ -129,32 +199,4 @@ m-analytics__menu {
}
}
}
// ----------------------------------------
padding: 16px 16px 16px 16px;
flex: 1 1 0px;
i {
display: none;
}
.catContainer {
cursor: pointer;
.cat {
a {
display: block;
padding: 6px 0;
text-decoration: none;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-200);
}
}
a.selected,
&:hover a {
@include m-theme() {
color: themed($m-blue);
}
}
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Session } from '../../../services/session';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { SidebarMenuComponent } from './sidebar-menu.component';
describe('SidebarMenuComponent', () => {
let component: SidebarMenuComponent;
let fixture: ComponentFixture<SidebarMenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SidebarMenuComponent],
imports: [RouterTestingModule],
providers: [{ provide: Session, useValue: sessionMock }],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SidebarMenuComponent);
component = fixture.componentInstance;
// component.user = sessionMock.user;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Session } from '../../../services/session';
import menuCategories from './categories.default';
interface MenuCategory {
category: MenuLink;
subcategories?: MenuLink[];
expanded?: boolean;
}
export { MenuCategory };
interface MenuLink {
id: string;
label: string;
permissions?: string[];
permissionGranted?: boolean;
path?: string;
}
export { MenuLink };
@Component({
selector: 'm-sidebarMenu',
templateUrl: './sidebar-menu.component.html',
})
export class SidebarMenuComponent implements OnInit {
cats: MenuCategory[] = menuCategories;
mobileMenuExpanded = false;
activeCat;
minds: Minds;
user;
userRoles: string[] = ['user'];
constructor(public route: ActivatedRoute, public session: Session) {}
ngOnInit() {
this.minds = window.Minds;
this.user = this.session.getLoggedInUser();
this.getUserRoles();
this.grantPermissionsAndFindActiveCat();
}
getUserRoles() {
if (this.session.isAdmin()) {
this.userRoles.push('admin');
}
// TODO: define & handle other userRole options, e.g. pro, loggedIn
}
grantPermissionsAndFindActiveCat() {
this.cats.forEach(catObj => {
catObj.category['permissionGranted'] = catObj.category.permissions
? this.checkForRoleMatch(catObj.category.permissions)
: true;
if (catObj.subcategories) {
catObj.subcategories.forEach(subCat => {
subCat['permissionGranted'] = subCat.permissions
? this.checkForRoleMatch(subCat.permissions)
: true;
});
}
if (location.pathname.indexOf(catObj.category.path) !== -1) {
catObj.category['expanded'] = true;
this.activeCat = catObj;
} else {
catObj.category['expanded'] = false;
}
});
}
checkForRoleMatch(permissionsArray) {
return permissionsArray.some(role => this.userRoles.includes(role));
}
}
......@@ -51,7 +51,6 @@ import { PageviewsCardComponent } from './components/cards/pageviews/pageviews.c
import { PageviewsChartComponent } from './components/charts/pageviews/pageviews.component';
import { AnalyticsDashboardComponent } from './v2/dashboard.component';
import { AnalyticsLayoutChartComponent } from './v2/layouts/layout-chart/layout-chart.component';
import { AnalyticsLayoutTableComponent } from './v2/layouts/layout-table/layout-table.component';
import { AnalyticsLayoutSummaryComponent } from './v2/layouts/layout-summary/layout-summary.component';
import { AnalyticsMetricsComponent } from './v2/components/metrics/metrics.component';
import { AnalyticsFiltersComponent } from './v2/components/filters/filters.component';
......@@ -63,7 +62,6 @@ import { SearchModule } from '../search/search.module';
import { AnalyticsSearchComponent } from './v2/components/search/search.component';
import { FormsModule } from '@angular/forms';
import { AnalyticsSearchSuggestionsComponent } from './v2/components/search-suggestions/search-suggestions.component';
import { AnalyticsMenuComponent } from './v2/components/menu/menu.component';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -163,7 +161,6 @@ const routes: Routes = [
Graph,
AnalyticsDashboardComponent,
AnalyticsLayoutChartComponent,
AnalyticsLayoutTableComponent,
AnalyticsLayoutSummaryComponent,
AnalyticsMetricsComponent,
AnalyticsFiltersComponent,
......@@ -172,7 +169,6 @@ const routes: Routes = [
AnalyticsTableComponent,
AnalyticsSearchComponent,
AnalyticsSearchSuggestionsComponent,
AnalyticsMenuComponent,
],
providers: [AnalyticsDashboardService],
})
......
const categories: Array<any> = [
// {
// id: 'summary',
// label: 'Summary',
// permissions: ['admin', 'user'],
// metrics: [],
// },
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
metrics: [
'active_users',
'signups',
'unique_visitors',
'pageviews',
'impressions',
'retention',
],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
metrics: ['total', 'pageviews', 'active_referrals', 'customers'],
},
// {
// id: 'engagement',
// label: 'Engagement',
// permissions: ['admin', 'user'],
// metrics: ['posts', 'votes', 'comments', 'reminds', 'subscribers', 'tags'],
// },
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
metrics: ['top_content', 'top_channels'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// metrics: ['top_referrers'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_tokens'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
];
export default categories;
const chartPalette: Array<any> = [
{
id: 'm-white',
themeMap: ['#fff', '#161616'],
},
{
id: 'm-transparent',
themeMap: ['rgba(0,0,0,0)', 'rgba(0,0,0,0)'],
},
{
id: 'm-grey-50',
themeMap: ['rgba(232,232,232,1)', 'rgba(53,53,53,1)'],
},
{
id: 'm-grey-70',
themeMap: ['#eee', '#333'],
},
{
id: 'm-grey-130',
themeMap: ['#ccc', '#555'],
},
{
id: 'm-grey-160',
themeMap: ['#bbb', '#555'],
},
{
id: 'm-grey-300',
themeMap: ['#999', '#666'],
},
{
id: 'm-blue',
themeMap: ['#4690df', '#44aaff'],
},
{
id: 'm-red-dark',
themeMap: ['#c62828', '#e57373'],
},
{
id: 'm-amber-dark',
themeMap: ['#ffa000', '#ffecb3'],
},
{
id: 'm-green-dark',
themeMap: ['#388e3c', '#8bc34a'],
},
{
id: 'm-blue-grey-500',
themeMap: ['#607d8b', '#607d8b'],
},
];
const chartPalette = {
segmentColorIds: [
// Colors for up to 6 segments
'm-blue',
'm-grey-160',
'm-amber-dark',
'm-green-dark',
'm-red-dark',
'm-blue-grey-500',
],
themeMaps: [
{
id: 'm-white',
themeMap: ['#fff', '#232323'],
},
{
id: 'm-grey-50',
themeMap: ['rgba(232,232,232,1)', 'rgba(47,47,47,1)'], // 222
},
{
id: 'm-grey-70',
themeMap: ['#eee', '#404040'], // 333 before 5% lighten
},
{
id: 'm-grey-130',
themeMap: ['#ccc', '#515151'], // 444
},
{
id: 'm-grey-160',
themeMap: ['#bbb', '#626262'], // 555
},
{
id: 'm-grey-300',
themeMap: ['#999', '#737373'], // 666
},
{
id: 'm-blue',
themeMap: ['#4690df', '#5db6ff'], // 44aaff
},
{
id: 'm-red-dark',
themeMap: ['#c62828', '#e98989'], // e57373
},
{
id: 'm-amber-dark',
themeMap: ['#ffa000', '#fff2cc'], // ffecb3
},
{
id: 'm-green-dark',
themeMap: ['#388e3c', '#97c95d'], // 8bc34a
},
{
id: 'm-blue-grey-500',
themeMap: ['#607d8b', '#6b8a99'], // 607d8b
},
],
};
export default chartPalette;
<!-- TODO: Make this into a different component -->
<!-- <m-chart [buckets]="(vm$.thecurrentvisualisation" | async)></m-chart> -->
<!-- TODO: then all this becomes m-plotlyChart -->
<!-- <div *ngIf="vm$ | async as vm"> -->
<div>
<div #graphDiv id="graphDiv"></div>
<!-- <plotly-plot
<div
#chartContainer
class="m-analyticsChart__chartContainer"
[ngClass]="{ isTouchDevice: isTouchDevice }"
>
<plotly-plot
*ngIf="init"
#graphDiv
id="graphDiv"
[divId]="graphDiv"
[data]="data"
[layout]="layout"
[config]="config"
[useResizeHandler]="true"
[style]="{ position: 'relative' }"
[useResizeHandler]="true"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
(afterPlot)="afterPlot()"
(plotly_click)="onClick($event)"
>
</plotly-plot> -->
<!-- <div class="hoverInfo__row">
{{ hoverInfo.date | date: selectedTimespan.datePipe }}
</div> -->
</plotly-plot>
</div>
<div #hoverInfoDiv id="hoverInfoDiv" class="hoverInfoDiv">
<div class="hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div [ngSwitch]="selectedMetric.unit" class="hoverInfo__row--primary">
<div #hoverInfoDiv id="hoverInfoDiv" class="m-analyticsChart__hoverInfoDiv">
<i *ngIf="isTouchDevice" class="material-icons" (click)="onUnhover($event)"
>close</i
>
<div class="m-analyticsChart__hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div
[ngSwitch]="selectedMetric?.unit"
class="m-analyticsChart__hoverInfo__row--primary"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number }} {{ selectedMetric.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ selectedMetric?.unit }}
</ng-template>
</div>
<div class="m-analyticsChart__hoverInfo__row" *ngIf="isComparison">
vs
<ng-container
[ngSwitch]="selectedMetric?.unit"
class="m-analyticsChart__hoverInfo__row"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number }} {{ selectedMetric.label | lowercase }}
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
{{ hoverInfo.comparisonValue | currency }}
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ selectedMetric.unit }}
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
</ng-template>
</div>
<div class="hoverInfo__row" *ngIf="isComparison">
vs
<ng-container [ngSwitch]="selectedMetric.unit" class="hoverInfo__row">
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
{{ selectedMetric.unit }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
</div>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
</div>
</div>
m-analytics__chart {
display: block;
position: relative;
margin-left: 40px;
.js-plotly-plot,
.plot-container {
height: 44vh;
min-height: 44vh;
display: block;
}
}
#graphDiv {
display: block;
position: relative;
g,
g > * {
......@@ -24,7 +28,7 @@ m-analytics__chart {
}
}
.hoverInfoDiv {
.m-analyticsChart__hoverInfoDiv {
width: 160px;
padding: 12px;
position: absolute;
......@@ -37,20 +41,49 @@ m-analytics__chart {
@include m-theme() {
background-color: themed($m-white);
box-shadow: 0 0 4px rgba(themed($m-black), 0.3);
color: themed($m-grey-200);
color: themed($m-grey-300);
}
[class*='hoverInfo__row'] {
[class*='m-analyticsChart__hoverInfo__row'] {
padding-bottom: 4px;
font-weight: 300;
&:last-of-type {
padding-top: 2px;
}
}
.hoverInfo__row--primary {
.m-analyticsChart__hoverInfo__row--primary {
font-weight: 400;
font-size: 15px;
// font-weight: bold;
@include m-theme() {
color: themed($m-grey-600);
}
}
i {
display: none;
font-size: 15px;
position: absolute;
cursor: pointer;
top: 10px;
right: 10px;
transition: color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
color: themed($m-grey-300);
}
&:hover {
@include m-theme() {
color: themed($m-grey-500);
}
}
}
}
.isTouchDevice .m-analyticsChart__hoverInfoDiv i {
display: block;
}
@media screen and (max-width: $min-tablet) {
m-analytics__chart {
margin-left: 16px;
}
}
// Working version: https://codepen.io/omadrid/pen/NWKZYrV?editors=0010
import {
Component,
OnInit,
......@@ -22,9 +20,10 @@ import {
import * as Plotly from 'plotly.js';
import chartPalette from '../../chart-palette.default';
import { ThemeService } from '../../../../../common/services/theme.service';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
interface TimespanExtended extends TimespanBase {
tickFormat?: string;
xTickFormat?: string;
datePipe?: string;
}
export { TimespanExtended as Timespan };
......@@ -34,11 +33,14 @@ export { TimespanExtended as Timespan };
templateUrl: 'chart.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsChartComponent implements OnInit, OnDestroy {
@ViewChild('graphDiv', { static: true }) graphDivEl: ElementRef;
export class AnalyticsChartComponent implements OnDestroy, OnInit {
@ViewChild('graphDiv', { static: true }) graphDiv;
@ViewChild('hoverInfoDiv', { static: true }) hoverInfoDivEl: ElementRef;
@ViewChild('chartContainer', { static: true }) chartContainer: ElementRef;
isTouchDevice: boolean;
init: boolean = false;
graphDiv: any;
hoverInfoDiv: any;
hoverInfo: any = {};
......@@ -59,48 +61,41 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
segments: Buckets[];
isComparison: boolean = false;
data: Array<any> = [];
layout: any;
config: any = {
data = [];
layout;
config = {
displayModeBar: false,
// responsive: true,
};
segmentLength: number;
pointsPerSegment = 1;
hoverPoint: number;
lineRange: Array<any>;
newLineRange = true;
markerOpacities: Array<number> = [];
shapes: Array<any> = [];
markerFills;
shapes = [];
timespanFormats = [
{ interval: 'day', tickFormat: '%m/%d', datePipe: 'EEE MMM d, y' },
{ interval: 'month', tickFormat: '%m/%y', datePipe: 'MMM y' },
{ interval: 'day', xTickFormat: '%m/%d', datePipe: 'EEE MMM d, y' },
{ interval: 'month', xTickFormat: '%m/%Y', datePipe: 'MMM y' },
];
datePipe: string = this.timespanFormats[0].datePipe;
tickFormat: string = this.timespanFormats[0].tickFormat;
xTickFormat: string = this.timespanFormats[0].xTickFormat;
// yTickPrefix: string = '';
yTickFormat: string = '';
// ***********************************************************************************
// ***********************************************************
constructor(
private analyticsService: AnalyticsDashboardService,
private themeService: ThemeService,
protected cd: ChangeDetectorRef
protected cd: ChangeDetectorRef,
private hostElement: ElementRef
) {}
ngOnInit() {
this.graphDiv = this.graphDivEl.nativeElement;
this.isTouchDevice = isMobileOrTablet();
this.hoverInfoDiv = this.hoverInfoDivEl.nativeElement;
this.metricSubscription = this.selectedMetric$.subscribe(metric => {
this.selectedMetric = metric;
try {
this.initPlot();
} catch (err) {
console.log(err);
}
this.detectChanges();
});
this.timespansSubscription = this.analyticsService.timespans$.subscribe(
timespans => {
this.selectedTimespan = timespans.find(
......@@ -112,33 +107,60 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
t => t.interval === this.selectedTimespan.interval
) || this.timespanFormats[0];
this.tickFormat = timespanFormat.tickFormat;
this.xTickFormat = timespanFormat.xTickFormat;
this.datePipe = timespanFormat.datePipe;
if (this.init) {
this.layout.xaxis.tickformat = this.xTickFormat;
}
this.detectChanges();
}
);
this.themeSubscription = this.themeService.isDark$.subscribe(isDark => {
this.isDark = isDark;
this.applyDimensions();
// this.relayout(this.getLayout());
// this.restyle(this.getData());
if (this.init) {
this.getData();
this.getLayout();
}
this.detectChanges();
});
this.metricSubscription = this.selectedMetric$.subscribe(metric => {
this.init = false;
this.selectedMetric = metric;
if (metric.unit && metric.unit === 'usd') {
this.yTickFormat = '$.2f';
}
try {
this.initPlot();
} catch (err) {
console.log(err);
}
this.detectChanges();
});
}
swapSegmentColors() {
const tempPaletteItem = chartPalette.segmentColorIds[0];
chartPalette.segmentColorIds[0] = chartPalette.segmentColorIds[1];
chartPalette.segmentColorIds[1] = tempPaletteItem;
}
initPlot() {
this.data = [];
this.shapes = [];
this.markerOpacities = [];
this.segments = this.selectedMetric.visualisation.segments;
this.segmentLength = this.segments[0].buckets.length;
if (this.segments.length === 2) {
this.isComparison = true;
for (let i = 0; i < this.segmentLength; i++) {
this.markerOpacities[i] = 0;
// Reverse the segments so comparison line is layered behind current line
this.segments.reverse();
// Current line should be blue
this.swapSegmentColors();
}
this.pointsPerSegment = this.segments[0].buckets.length;
for (let i = 0; i < this.pointsPerSegment; i++) {
this.shapes[i] = {
type: 'line',
layer: 'below',
......@@ -147,213 +169,58 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
x1: this.segments[0].buckets[i].date,
y1: 0,
line: {
color: this.getColor('m-transparent'),
color: 'rgba(0, 0, 0, 0)',
width: 2,
},
};
// if (this.selectedMetric.unit === 'usd'){
// this.segments.forEach(segment =>{ segment.buckets})
// }
}
this.data = this.getData();
this.layout = this.getLayout();
Plotly.newPlot('graphDiv', this.data, this.layout, this.config);
this.setLineHeights();
this.graphDiv.on('plotly_hover', $event => {
this.onHover($event);
});
this.graphDiv.on('plotly_unhover', $event => {
this.onUnhover($event);
});
this.getData();
this.getLayout();
this.init = true;
this.detectChanges();
}
// ----------------------------------------------
// EVENT: HOVER
// ----------------------------------------------
onHover($event) {
// TODO: return if filters.component filter is expanded
this.hoverPoint = $event.points[0].pointIndex;
this.showMarkers();
this.showShapes();
this.positionHoverInfo($event);
this.populateHoverInfo();
this.showHoverInfo();
this.detectChanges();
}
// ----------------------------------------------
// EVENT: UNHOVER
// ----------------------------------------------
onUnhover($event) {
this.hideMarkers();
this.hideShapes();
this.hideHoverInfo();
}
showMarkers() {
this.markerOpacities[this.hoverPoint] = 1;
Plotly.restyle(this.graphDiv, {
marker: { opacity: this.markerOpacities },
});
}
hideMarkers() {
this.markerOpacities[this.hoverPoint] = 0;
Plotly.restyle(this.graphDiv, {
marker: { opacity: this.markerOpacities },
});
}
showShapes() {
this.layout.shapes[this.hoverPoint].line.color = this.getColor('m-grey-50');
this.relayout(this.layout);
}
hideShapes() {
// HIDE VERTICAL LINE
this.layout.shapes[this.hoverPoint].line.color = this.getColor(
'm-transparent'
);
this.relayout(this.layout);
}
populateHoverInfo() {
// TODO: format value strings here and remove ngSwitch from template?
this.hoverInfo['date'] = this.segments[0].buckets[this.hoverPoint].date;
this.hoverInfo['value'] =
this.selectedMetric.unit !== 'usd'
? this.segments[0].buckets[this.hoverPoint].value
: this.segments[0].buckets[this.hoverPoint].value / 100;
if (this.isComparison && this.segments[1]) {
this.hoverInfo['comparisonValue'] =
this.selectedMetric.unit !== 'usd'
? this.segments[1].buckets[this.hoverPoint].value
: this.segments[1].buckets[this.hoverPoint].value / 100;
this.hoverInfo['comparisonDate'] = this.segments[1].buckets[
this.hoverPoint
].date;
}
}
positionHoverInfo($event) {
const xAxis = $event.points[0].xaxis,
yAxis = $event.points[0].yaxis,
tooltipXDist = xAxis.d2p($event.points[0].x) + 16,
tooltipYDist = yAxis.d2p($event.points[0].y) + 16;
// if (this.hoverPoint < this.segmentLength / 2) {
this.hoverInfoDiv.style.top = tooltipYDist + yAxis._offset + 'px';
this.hoverInfoDiv.style.left = tooltipXDist + xAxis._offset + 'px';
// } else {
// // TODO move the second half of tooltips to the left of points
// // TODO also shift down/up if with x% of rangeMin/rangeMax???
// this.hoverInfoDiv.style.top = tooltipYDist + xAxis._offset + 'px';
// this.hoverInfoDiv.style.left = tooltipXDist + yAxis._offset + 'px';
// }
}
showHoverInfo() {
this.hoverInfoDiv.style.opacity = 1;
}
hideHoverInfo() {
this.hoverInfoDiv.style.opacity = 0;
}
// UTILITY \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/
update(data, layout) {
this.data = data;
this.layout = layout;
Plotly.update('graphDiv', data, layout);
}
restyle(data) {
this.data = data;
Plotly.restyle(this.graphDiv, data);
this.detectChanges();
}
relayout(layout) {
this.layout = layout;
Plotly.relayout(this.graphDiv, layout);
this.detectChanges();
}
unpack(rows, key) {
return rows.map(row => {
if (key === 'date') {
return row[key].slice(0, 10);
} else if (this.selectedMetric.unit === 'usd') {
return row[key] / 100;
} else {
return row[key];
getData() {
this.markerFills = [];
this.segments.forEach((segment, index) => {
const segmentMarkerFills = [];
for (let i = 0; i < this.pointsPerSegment; i++) {
segmentMarkerFills[i] = this.getColor('m-white');
}
this.markerFills.push(segmentMarkerFills);
});
}
getColor(colorId) {
const palette = chartPalette;
let colorCode = '#607d8b';
if (palette.find(color => color.id === colorId)) {
colorCode = palette.find(color => color.id === colorId).themeMap[
+this.isDark
];
}
return colorCode;
}
setLineHeights() {
this.shapes.forEach(shape => {
shape.y0 = this.graphDiv.layout.yaxis.range[0];
shape.y1 = this.graphDiv.layout.yaxis.range[1];
});
this.relayout(this.getLayout());
}
getData() {
const globalSegmentSettings: any = {
const globalSegmentSettings = {
type: 'scatter',
mode: 'lines+markers',
line: {
width: 2,
width: 1,
dash: 'solid',
},
marker: {
size: 10,
opacity: 0,
size: 7,
},
showlegend: false,
hoverinfo: 'text',
x: this.unpack(this.segments[0].buckets, 'date'),
};
// COLORS FOR UP TO 6 SEGMENTS
const segmentColorIds = [
'm-blue',
'm-grey-160',
'm-amber-dark',
'm-green-dark',
'm-red-dark',
'm-blue-grey-500',
];
this.segments.forEach((s, i) => {
const segment = {
...globalSegmentSettings,
line: {
...globalSegmentSettings.line,
color: this.getColor(segmentColorIds[i]),
color: this.getColor(chartPalette.segmentColorIds[i]),
},
marker: {
...globalSegmentSettings.marker,
color: this.markerFills[i],
line: {
color: this.getColor(chartPalette.segmentColorIds[i]),
width: 1,
},
},
y: this.unpack(this.segments[i].buckets, 'value'),
};
......@@ -361,17 +228,17 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
this.data[i] = segment;
});
if (this.segments.length === 2) {
this.isComparison = true;
this.data[1].line.dash = 'dot';
if (this.isComparison) {
this.data[0].line.dash = 'dot';
}
return this.data;
}
getLayout() {
return {
this.layout = {
width: 0,
height: 0,
autoexpand: 'true',
autosize: 'true',
hovermode: 'x',
paper_bgcolor: this.getColor('m-white'),
plot_bgcolor: this.getColor('m-white'),
......@@ -379,8 +246,8 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
family: 'Roboto',
},
xaxis: {
tickformat: this.tickFormat,
tickmode: 'array', // || linear || auto
tickformat: this.xTickFormat,
tickmode: 'array',
tickson: 'labels',
tickcolor: this.getColor('m-grey-130'),
tickangle: -45,
......@@ -393,9 +260,14 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
linewidth: 1,
zeroline: false,
fixedrange: true,
// automargin: true,
// rangemode: 'nonnegative',
},
yaxis: {
ticks: '',
tickformat: this.yTickFormat,
tickmode: 'array',
tickson: 'labels',
showgrid: true,
gridcolor: this.getColor('m-grey-70'),
zeroline: false,
......@@ -405,27 +277,167 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
color: this.getColor('m-grey-130'),
},
fixedrange: true,
// automargin: true,
// rangemode: 'nonnegative',
},
margin: {
t: 16,
b: 40,
l: 24,
r: 40,
// pad: 16,
b: 80,
l: 0,
r: 80,
pad: 16,
},
shapes: this.shapes,
};
}
onHover($event) {
this.hoverPoint = $event.points[0].pointIndex;
this.addMarkerFill();
this.showShape($event);
this.positionHoverInfo($event);
this.populateHoverInfo();
this.hoverInfoDiv.style.opacity = 1;
this.detectChanges();
}
onUnhover($event) {
this.emptyMarkerFill();
this.hideShape();
this.hoverInfoDiv.style.opacity = 0;
this.detectChanges();
}
onClick($event) {
if (!this.isTouchDevice) {
return;
}
// TODO: use this for non-hover devices
}
addMarkerFill() {
this.data.forEach((segment, i) => {
this.markerFills[i][this.hoverPoint] = this.getColor(
chartPalette.segmentColorIds[i]
);
});
}
emptyMarkerFill() {
this.data.forEach((segment, i) => {
this.markerFills[i][this.hoverPoint] = this.getColor('m-white');
segment.marker.color = this.markerFills[i];
});
}
showShape($event) {
const hoverLine = this.shapes[this.hoverPoint];
// Without this, entire graph resizes on every hover
if (this.newLineRange) {
this.newLineRange = false;
this.lineRange = $event.yaxes[0].range;
}
hoverLine.y0 = this.lineRange[0];
hoverLine.y1 = this.lineRange[1] * 0.99;
hoverLine.line.color = this.getColor('m-grey-70');
this.layout.shapes = this.shapes;
}
hideShape() {
this.shapes[this.hoverPoint].line.color = 'rgba(0, 0, 0, 0)';
this.layout.shapes = this.shapes;
}
populateHoverInfo() {
const pt = this.isComparison ? 1 : 0;
// TODO: format value strings here and remove ngSwitch from template?
this.hoverInfo['date'] = this.segments[pt].buckets[this.hoverPoint].date;
this.hoverInfo['value'] =
this.selectedMetric.unit !== 'usd'
? this.segments[pt].buckets[this.hoverPoint].value
: this.segments[pt].buckets[this.hoverPoint].value / 100;
if (this.isComparison && this.segments[1]) {
this.hoverInfo['comparisonValue'] =
this.selectedMetric.unit !== 'usd'
? this.segments[0].buckets[this.hoverPoint].value
: this.segments[0].buckets[this.hoverPoint].value / 100;
this.hoverInfo['comparisonDate'] = this.segments[0].buckets[
this.hoverPoint
].date;
}
}
positionHoverInfo($event) {
const pad = 16,
pt = this.isComparison ? 1 : 0,
xAxis = $event.points[pt].xaxis,
yAxis = $event.points[pt].yaxis,
pointXDist = xAxis.d2p($event.points[pt].x) + xAxis._offset,
pointYDist = yAxis.d2p($event.points[pt].y) + yAxis._offset,
plotRect = document
.querySelector('.js-plotly-plot')
.getBoundingClientRect(),
hoverInfoRect = this.hoverInfoDiv.getBoundingClientRect();
if (pointYDist < plotRect.height / 2) {
// If point is in top half of plot, hoverinfo should go beneath it
this.hoverInfoDiv.style.top = pointYDist + pad + 'px';
} else {
this.hoverInfoDiv.style.top =
pointYDist - pad - hoverInfoRect.height + 'px';
}
if (pointXDist < plotRect.width / 2) {
// If point is in left half of plot, hoverinfo should go on the right
this.hoverInfoDiv.style.left = pointXDist + pad + 'px';
} else {
this.hoverInfoDiv.style.left =
pointXDist - pad - hoverInfoRect.width + 'px';
}
}
@HostListener('window:resize')
applyDimensions() {
this.layout = {
...this.layout,
width: this.graphDiv.clientWidth - 32,
height: this.graphDiv.clientHeight - 32,
};
this.setLineHeights();
this.detectChanges();
if (this.init) {
// this.layout.width = this.hostElement.nativeElement.clientWidth;
// this.layout.height = this.hostElement.nativeElement.clientHeight;
this.layout.width = this.chartContainer.nativeElement.clientWidth;
//-32; //- 56;
this.layout.height = this.chartContainer.nativeElement.clientHeight;
this.newLineRange = true;
this.detectChanges();
}
}
// * UTILITY -----------------------------------
unpack(rows, key) {
return rows.map(row => {
if (key === 'date') {
return row[key].slice(0, 10);
} else if (this.selectedMetric.unit === 'usd') {
return row[key] / 100;
} else {
return row[key];
}
});
}
getColor(colorId) {
const palette = chartPalette.themeMaps;
let colorCode = '#607d8b';
if (palette.find(color => color.id === colorId)) {
colorCode = palette.find(color => color.id === colorId).themeMap[
+this.isDark
];
}
return colorCode;
}
detectChanges() {
......