Web console: segment timeline (#8202)

* Add segment timeline chart

* fix file

* Fix bugs: no data handling & scaling problems

* resolve conflict

* changed package-lock

* do not show by default

* trust the interop

* stricter type fixes

* fix sasslint
This commit is contained in:
Vadim Ogievetsky 2019-07-30 22:35:30 -07:00 committed by Fangjin Yang
parent ddec5ea82e
commit 645fca53d8
14 changed files with 1737 additions and 72 deletions

File diff suppressed because it is too large Load Diff

View File

@ -58,6 +58,7 @@
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.2.0",
"core-js": "^3.1.4",
"d3": "^5.9.7",
"d3-array": "^2.2.0",
"druid-console": "^0.0.2",
"file-saver": "^2.0.2",
@ -82,6 +83,7 @@
"@babel/preset-env": "^7.5.5",
"@testing-library/react": "^8.0.7",
"@types/classnames": "^2.2.9",
"@types/d3": "^5.7.2",
"@types/d3-array": "^2.0.0",
"@types/enzyme": "^3.10.3",
"@types/enzyme-adapter-react-16": "^1.0.5",

View File

@ -35,3 +35,4 @@ export * from './view-control-bar/view-control-bar';
export * from './clearable-input/clearable-input';
export * from './refresh-button/refresh-button';
export * from './timed-button/timed-button';
export * from './segment-timeline/segment-timeline';

View File

@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.segment-timeline {
display: grid;
grid-template-columns: 85% 15%;
height: 100%;
margin-top: 10px;
.loader {
width: 85%;
}
.loading-error {
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.bar-chart-tooltip {
margin-top: 15px;
text-align: center;
div {
display: inline-block;
width: 200px;
}
}
.no-data-text {
position: absolute;
left: 30vw;
top: 15vh;
font-size: 20px;
}
.side-control {
padding-left: 1vw;
padding-top: 5vh;
}
}

View File

@ -0,0 +1,496 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FormGroup, HTMLSelect, Radio, RadioGroup } from '@blueprintjs/core';
import * as d3 from 'd3';
import { AxisScale } from 'd3';
import React from 'react';
import { formatBytes, queryDruidSql, QueryManager } from '../../utils/index';
import { StackedBarChart } from '../../visualization/stacked-bar-chart';
import { Loader } from '../loader/loader';
import './segment-timeline.scss';
interface SegmentTimelineProps extends React.Props<any> {
chartHeight: number;
chartWidth: number;
}
interface SegmentTimelineState {
data?: Record<string, any>;
datasources: string[];
stackedData?: Record<string, BarUnitData[]>;
singleDatasourceData?: Record<string, Record<string, BarUnitData[]>>;
activeDatasource: string | null;
activeDataType: string; // "countData" || "sizeData"
dataToRender: BarUnitData[];
timeSpan: number; // by months
loading: boolean;
error?: string;
xScale: AxisScale<Date> | null;
yScale: AxisScale<number> | null;
dStart: Date;
dEnd: Date;
}
interface BarChartScales {
xScale: AxisScale<Date>;
yScale: AxisScale<number>;
}
export interface BarUnitData {
x: number;
y: number;
y0?: number;
width: number;
datasource: string;
color: string;
}
export interface BarChartMargin {
top: number;
right: number;
bottom: number;
left: number;
}
export class SegmentTimeline extends React.Component<SegmentTimelineProps, SegmentTimelineState> {
private dataQueryManager: QueryManager<null, any>;
private datasourceQueryManager: QueryManager<null, any>;
private colors = [
'#b33040',
'#d25c4d',
'#f2b447',
'#d9d574',
'#4FAA7E',
'#57ceff',
'#789113',
'#098777',
'#b33040',
'#d2757b',
'#f29063',
'#d9a241',
'#80aa61',
'#c4ff9e',
'#915412',
'#87606c',
];
private chartMargin = { top: 20, right: 10, bottom: 20, left: 10 };
constructor(props: SegmentTimelineProps) {
super(props);
const dStart = new Date();
const dEnd = new Date();
dStart.setMonth(dStart.getMonth() - 3);
this.state = {
data: {},
datasources: [],
stackedData: {},
singleDatasourceData: {},
dataToRender: [],
activeDatasource: null,
activeDataType: 'countData',
timeSpan: 3,
loading: true,
xScale: null,
yScale: null,
dEnd: dEnd,
dStart: dStart,
};
this.dataQueryManager = new QueryManager({
processQuery: async () => {
const { timeSpan } = this.state;
const query = `SELECT "start", "end", "datasource", COUNT(*) AS "count", sum(size) as "total_size"
FROM sys.segments
WHERE "start" > time_format(TIMESTAMPADD(MONTH, -${timeSpan}, current_timestamp), 'yyyy-MM-dd''T''hh:mm:ss.SSS')
GROUP BY 1, 2, 3
ORDER BY "start" DESC`;
const resp: any[] = await queryDruidSql({ query });
const data = this.processRawData(resp);
const stackedData = this.calculateStackedData(data);
const singleDatasourceData = this.calculateSingleDatasourceData(data);
return { data, stackedData, singleDatasourceData };
},
onStateChange: ({ result, loading, error }) => {
this.setState({
data: result ? result.data : undefined,
stackedData: result ? result.stackedData : undefined,
singleDatasourceData: result ? result.singleDatasourceData : undefined,
loading,
error,
});
},
});
this.datasourceQueryManager = new QueryManager({
processQuery: async () => {
const query = `SELECT DISTINCT "datasource" FROM sys.segments`;
const resp: any[] = await queryDruidSql({ query });
const data = resp.map((r: any) => r.datasource);
return data;
},
onStateChange: ({ result }) => {
if (result == null) result = [];
this.setState({
datasources: result,
});
},
});
}
componentDidMount(): void {
this.dataQueryManager.runQuery(null);
this.datasourceQueryManager.runQuery(null);
}
componentWillUnmount(): void {
this.dataQueryManager.terminate();
this.datasourceQueryManager.terminate();
}
componentDidUpdate(prevProps: SegmentTimelineProps, prevState: SegmentTimelineState): void {
const { activeDatasource, activeDataType, singleDatasourceData, stackedData } = this.state;
if (
prevState.data !== this.state.data ||
prevState.activeDataType !== this.state.activeDataType ||
prevState.activeDatasource !== this.state.activeDatasource ||
prevProps.chartWidth !== this.props.chartWidth ||
prevProps.chartHeight !== this.props.chartHeight
) {
const scales: BarChartScales | undefined = this.calculateScales();
let dataToRender: BarUnitData[] | undefined;
dataToRender = activeDatasource
? singleDatasourceData
? singleDatasourceData[activeDataType][activeDatasource]
: undefined
: stackedData
? stackedData[activeDataType]
: undefined;
if (scales && dataToRender) {
this.setState({
dataToRender,
xScale: scales.xScale,
yScale: scales.yScale,
});
}
}
}
private processRawData(data: any) {
if (data === null) return [];
const countData: Record<string, any> = {};
const sizeData: Record<string, any> = {};
data.forEach((entry: any) => {
const start = entry.start;
const day = start.split('T')[0];
const datasource = entry.datasource;
const count = entry.count;
const segmentSize = entry['total_size'];
if (countData[day] === undefined) {
countData[day] = {
day,
[datasource]: count,
total: count,
};
sizeData[day] = {
day,
[datasource]: segmentSize,
total: segmentSize,
};
} else {
const countDataEntry = countData[day][datasource];
countData[day][datasource] = count + (countDataEntry === undefined ? 0 : countDataEntry);
const sizeDataEntry = sizeData[day][datasource];
sizeData[day][datasource] = segmentSize + (sizeDataEntry === undefined ? 0 : sizeDataEntry);
countData[day].total += count;
sizeData[day].total += segmentSize;
}
});
const countDataArray = Object.keys(countData)
.reverse()
.map((time: any) => {
return countData[time];
});
const sizeDataArray = Object.keys(sizeData)
.reverse()
.map((time: any) => {
return sizeData[time];
});
return { countData: countDataArray, sizeData: sizeDataArray };
}
private calculateStackedData(data: Record<string, any>): Record<string, BarUnitData[]> {
const { datasources } = this.state;
const newStackedData: Record<string, BarUnitData[]> = {};
Object.keys(data).forEach((type: any) => {
const stackedData: any = data[type].map((d: any) => {
let y0 = 0;
return datasources.map((datasource: string, i) => {
const barUnitData = {
x: d.day,
y: d[datasource] === undefined ? 0 : d[datasource],
y0,
datasource,
color: this.colors[i],
};
y0 += d[datasource] === undefined ? 0 : d[datasource];
return barUnitData;
});
});
newStackedData[type] = stackedData.flat();
});
return newStackedData;
}
private calculateSingleDatasourceData(
data: Record<string, any>,
): Record<string, Record<string, BarUnitData[]>> {
const { datasources } = this.state;
const singleDatasourceData: Record<string, Record<string, BarUnitData[]>> = {};
Object.keys(data).forEach(dataType => {
singleDatasourceData[dataType] = {};
datasources.forEach((datasource, i) => {
const currentData = data[dataType];
if (currentData.length === 0) return;
const dataResult = currentData.map((d: any) => {
let y = 0;
if (d[datasource] !== undefined) {
y = d[datasource];
}
return {
x: d.day,
y,
datasource,
color: this.colors[i],
};
});
if (!dataResult.every((d: any) => d.y === 0)) {
singleDatasourceData[dataType][datasource] = dataResult;
}
});
});
return singleDatasourceData;
}
private calculateScales(): BarChartScales | undefined {
const { chartWidth, chartHeight } = this.props;
const {
data,
activeDataType,
activeDatasource,
singleDatasourceData,
dStart,
dEnd,
} = this.state;
if (!data || !Object.keys(data).length) return;
const activeData = data[activeDataType];
const xDomain: Date[] = [dStart, dEnd];
let yDomain: number[] = [
0,
activeData.length === 0
? 0
: activeData.reduce((max: any, d: any) => (max.total > d.total ? max : d)).total,
];
if (
activeDatasource !== null &&
singleDatasourceData![activeDataType][activeDatasource] !== undefined
) {
yDomain = [
0,
singleDatasourceData![activeDataType][activeDatasource].reduce((max: any, d: any) =>
max.y > d.y ? max : d,
).y,
];
}
const xScale: AxisScale<Date> = d3
.scaleTime()
.domain(xDomain)
.range([0, chartWidth - this.chartMargin.left - this.chartMargin.right]);
const yScale: AxisScale<number> = d3
.scaleLinear()
.rangeRound([chartHeight - this.chartMargin.top - this.chartMargin.bottom, 0])
.domain(yDomain);
return {
xScale,
yScale,
};
}
onTimeSpanChange = (e: any) => {
const dStart = new Date();
const dEnd = new Date();
dStart.setMonth(dStart.getMonth() - e);
this.setState({
timeSpan: e,
loading: true,
dStart,
dEnd,
});
this.dataQueryManager.rerunLastQuery();
};
formatTick = (n: number) => {
const { activeDataType } = this.state;
if (activeDataType === 'countData') {
return n.toString();
} else {
return formatBytes(n);
}
};
renderStackedBarChart() {
const { chartWidth, chartHeight } = this.props;
const {
loading,
dataToRender,
activeDataType,
error,
xScale,
yScale,
data,
activeDatasource,
dStart,
dEnd,
} = this.state;
if (loading) {
return (
<div>
<Loader loading={loading} />
</div>
);
}
if (error) {
return (
<div>
<span className={'no-data-text'}>Error when loading data: {error}</span>
</div>
);
}
if (xScale === null || yScale === null) {
return (
<div>
<span className={'no-data-text'}>Error when calculating scales</span>
</div>
);
}
if (data![activeDataType].length === 0) {
return (
<div>
<span className={'no-data-text'}>No data available for the time span selected</span>
</div>
);
}
if (
activeDatasource !== null &&
data![activeDataType].every((d: any) => d[activeDatasource] === undefined)
) {
return (
<div>
<span className={'no-data-text'}>
No data available for <i>{activeDatasource}</i>
</span>
</div>
);
}
const millisecondsPerDay = 24 * 60 * 60 * 1000;
const barCounts = (dEnd.getTime() - dStart.getTime()) / millisecondsPerDay;
const barWidth = (chartWidth - this.chartMargin.left - this.chartMargin.right) / barCounts;
return (
<StackedBarChart
dataToRender={dataToRender}
svgHeight={chartHeight}
svgWidth={chartWidth}
margin={this.chartMargin}
changeActiveDatasource={(e: string) => this.setState({ activeDatasource: e })}
activeDataType={activeDataType}
formatTick={(n: number) => this.formatTick(n)}
xScale={xScale}
yScale={yScale}
barWidth={barWidth}
/>
);
}
render(): JSX.Element {
const { datasources, activeDataType, activeDatasource, timeSpan } = this.state;
return (
<div className={'segment-timeline app-view'}>
{this.renderStackedBarChart()}
<div className={'side-control'}>
<FormGroup>
<RadioGroup
onChange={(e: any) => this.setState({ activeDataType: e.target.value })}
selectedValue={activeDataType}
>
<Radio label={'Segment count'} value={'countData'} />
<Radio label={'Total size'} value={'sizeData'} />
</RadioGroup>
</FormGroup>
<FormGroup label={'Datasource:'}>
<HTMLSelect
onChange={(e: any) =>
this.setState({
activeDatasource: e.target.value === 'all' ? null : e.target.value,
})
}
value={activeDatasource == null ? 'all' : activeDatasource}
fill
>
<option value={'all'}>Show all</option>
{datasources.map(d => {
return (
<option key={d} value={d}>
{d}
</option>
);
})}
</HTMLSelect>
</FormGroup>
<FormGroup label={'Period:'}>
<HTMLSelect
onChange={(e: any) => this.onTimeSpanChange(e.target.value)}
value={timeSpan}
fill
>
<option value={1}> 1 months</option>
<option value={3}> 3 months</option>
<option value={6}> 6 months</option>
<option value={9}> 9 months</option>
<option value={12}> 1 year</option>
</HTMLSelect>
</FormGroup>
</div>
</div>
);
}
}

View File

@ -16,6 +16,11 @@ exports[`data source view matches snapshot 1`] = `
onClick={[Function]}
text="Go to SQL"
/>
<Blueprint3.Switch
checked={false}
label="Show segment timeline"
onChange={[Function]}
/>
<Blueprint3.Switch
checked={false}
label="Show disabled"
@ -58,7 +63,7 @@ exports[`data source view matches snapshot 1`] = `
TrComponent={[Function]}
TrGroupComponent={[Function]}
aggregatedKey="_aggregated"
className=""
className="-striped -highlight full-height"
collapseOnDataChange={true}
collapseOnPageChange={true}
collapseOnSortingChange={true}

View File

@ -21,15 +21,25 @@
.data-sources-view {
height: 100%;
width: 100%;
overflow: auto;
.clickable-cell {
cursor: pointer;
}
.chart-container {
height: 40vh;
margin-bottom: 10px;
}
.ReactTable {
position: absolute;
top: $view-control-bar-height + $standard-padding;
top: 50%; //$view-control-bar-height + $standard-padding;
bottom: 0;
width: 100%;
&.full-height {
top: 50px;
}
}
}

View File

@ -19,6 +19,7 @@
import { Button, FormGroup, InputGroup, Intent, Switch } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import classNames from 'classnames';
import React from 'react';
import ReactTable, { Filter } from 'react-table';
@ -30,6 +31,7 @@ import {
ViewControlBar,
} from '../../components';
import { ActionIcon } from '../../components/action-icon/action-icon';
import { SegmentTimeline } from '../../components/segment-timeline/segment-timeline';
import { AsyncActionDialog, CompactionDialog, RetentionDialog } from '../../dialogs';
import { AppToaster } from '../../singletons/toaster';
import {
@ -123,6 +125,9 @@ export interface DatasourcesViewState {
dropReloadAction: 'drop' | 'reload';
dropReloadInterval: string;
hiddenColumns: LocalStorageBackedArray<string>;
showChart: boolean;
chartWidth: number;
chartHeight: number;
}
export class DatasourcesView extends React.PureComponent<
@ -175,6 +180,9 @@ GROUP BY 1`;
hiddenColumns: new LocalStorageBackedArray<string>(
LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
),
showChart: false,
chartWidth: window.innerWidth * 0.85,
chartHeight: window.innerHeight * 0.4,
};
this.datasourceQueryManager = new QueryManager({
@ -252,9 +260,31 @@ GROUP BY 1`;
});
}
private handleResize = () => {
this.setState({
chartWidth: window.innerWidth * 0.85,
chartHeight: window.innerHeight * 0.4,
});
};
private refresh = (auto: any): void => {
this.datasourceQueryManager.rerunLastQuery(auto);
// this looks ugly, but it forces the chart to re-render when refresh is clicked
this.setState(
{
showChart: !this.state.showChart,
},
() =>
this.setState({
showChart: !this.state.showChart,
}),
);
};
componentDidMount(): void {
const { noSqlMode } = this.props;
this.datasourceQueryManager.runQuery(noSqlMode);
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount(): void {
@ -572,6 +602,7 @@ GROUP BY 1`;
datasourcesFilter,
showDisabled,
hiddenColumns,
showChart,
} = this.state;
let data = datasources || [];
if (!showDisabled) {
@ -790,6 +821,7 @@ GROUP BY 1`;
},
]}
defaultPageSize={50}
className={classNames(`-striped -highlight`, showChart ? '' : 'full-height')}
/>
{this.renderDropDataAction()}
{this.renderEnableAction()}
@ -803,13 +835,15 @@ GROUP BY 1`;
render(): JSX.Element {
const { goToQuery, noSqlMode } = this.props;
const { showDisabled, hiddenColumns } = this.state;
const { showDisabled, hiddenColumns, showChart, chartHeight, chartWidth } = this.state;
return (
<div className="data-sources-view app-view">
<ViewControlBar label="Datasources">
<RefreshButton
onRefresh={auto => this.datasourceQueryManager.rerunLastQuery(auto)}
onRefresh={auto => {
this.refresh(auto);
}}
localStorageKey={LocalStorageKeys.DATASOURCES_REFRESH_RATE}
/>
{!noSqlMode && (
@ -819,6 +853,11 @@ GROUP BY 1`;
onClick={() => goToQuery(DatasourcesView.DATASOURCE_SQL)}
/>
)}
<Switch
checked={showChart}
label="Show segment timeline"
onChange={() => this.setState({ showChart: !showChart })}
/>
<Switch
checked={showDisabled}
label="Show disabled"
@ -830,6 +869,11 @@ GROUP BY 1`;
tableColumnsHidden={hiddenColumns.storedArray}
/>
</ViewControlBar>
{showChart && (
<div className={'chart-container'}>
<SegmentTimeline chartHeight={chartHeight} chartWidth={chartWidth} />
</div>
)}
{this.renderDatasourceTable()}
</div>
);

View File

@ -0,0 +1,90 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AxisScale } from 'd3-axis';
import React from 'react';
import { BarUnitData } from '../components/segment-timeline/segment-timeline';
import { BarUnit } from './bar-unit';
import { HoveredBarInfo } from './stacked-bar-chart';
interface BarGroupProps extends React.Props<any> {
dataToRender: BarUnitData[];
changeActiveDatasource: (e: string) => void;
formatTick: (e: number) => string;
xScale: AxisScale<Date>;
yScale: AxisScale<number>;
barWidth: number;
onHoverBar?: (e: any) => void;
offHoverBar?: () => void;
hoverOn?: HoveredBarInfo | null;
}
interface BarGroupState {}
export class BarGroup extends React.Component<BarGroupProps, BarGroupState> {
constructor(props: BarGroupProps) {
super(props);
this.state = {};
}
shouldComponentUpdate(nextProps: BarGroupProps, nextState: BarGroupState): boolean {
if (nextState === null) console.log(nextState);
return nextProps.hoverOn === this.props.hoverOn;
}
render(): JSX.Element[] | null {
const {
dataToRender,
changeActiveDatasource,
xScale,
yScale,
onHoverBar,
barWidth,
} = this.props;
if (dataToRender === undefined) return null;
return dataToRender.map((entry: BarUnitData, i: number) => {
const y0 = yScale(entry.y0 || 0) || 0;
const x = xScale(new Date(entry.x));
const y = yScale((entry.y0 || 0) + entry.y) || 0;
const height = y0 - y;
const barInfo: HoveredBarInfo = {
xCoordinate: x,
yCoordinate: y,
height,
datasource: entry.datasource,
xValue: entry.x,
yValue: entry.y,
};
return (
<BarUnit
key={i}
x={x}
y={y}
width={barWidth}
height={height}
style={{ fill: entry.color }}
onClick={() => changeActiveDatasource(entry.datasource)}
onHover={() => onHoverBar && onHoverBar(barInfo)}
/>
);
});
}
}

View File

@ -0,0 +1,21 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.bar-chart-unit {
transform: translateX(65px);
}

View File

@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import './bar-unit.scss';
interface BarChartUnitProps extends React.Props<any> {
x: number | undefined;
y: number;
width: number;
height: number;
style?: any;
onClick?: () => void;
onHover?: () => void;
offHover?: () => void;
}
interface BarChartUnitState {}
export class BarUnit extends React.Component<BarChartUnitProps, BarChartUnitState> {
constructor(props: BarChartUnitProps) {
super(props);
this.state = {};
}
render(): JSX.Element {
const { x, y, width, height, style, onClick, onHover, offHover } = this.props;
return (
<g
className={`bar-chart-unit`}
onClick={onClick}
onMouseOver={onHover}
onMouseLeave={offHover}
>
<rect x={x} y={y} width={width} height={height} style={style} />
</g>
);
}
}

View File

@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as d3 from 'd3';
import React from 'react';
interface ChartAxisProps extends React.Props<any> {
transform: string;
scale: any;
className?: string;
}
interface ChartAxisState {}
export class ChartAxis extends React.Component<ChartAxisProps, ChartAxisState> {
constructor(props: ChartAxisProps) {
super(props);
this.state = {};
}
render(): JSX.Element {
const { transform, scale, className } = this.props;
return (
<g
className={`axis ${className}`}
transform={transform}
ref={node => d3.select(node).call(scale)}
/>
);
}
}

View File

@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.bar-chart {
.hovered-bar {
fill: transparent;
stroke: #ffffff;
stroke-width: 1.5px;
transform: translateX(65px);
}
.gridline-x {
line {
stroke-dasharray: 5, 5;
opacity: 0.5;
}
}
}

View File

@ -0,0 +1,172 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as d3 from 'd3';
import { AxisScale } from 'd3';
import React from 'react';
import { BarChartMargin, BarUnitData } from '../components/segment-timeline/segment-timeline';
import { BarGroup } from './bar-group';
import { ChartAxis } from './chart-axis';
import './stacked-bar-chart.scss';
interface StackedBarChartProps extends React.Props<any> {
svgWidth: number;
svgHeight: number;
margin: BarChartMargin;
activeDataType?: string;
dataToRender: BarUnitData[];
changeActiveDatasource: (e: string) => void;
formatTick: (e: number) => string;
xScale: AxisScale<Date>;
yScale: AxisScale<number>;
barWidth: number;
}
interface StackedBarChartState {
width: number;
height: number;
hoverOn?: HoveredBarInfo | null;
}
export interface HoveredBarInfo {
xCoordinate?: number;
yCoordinate?: number;
height?: number;
width?: number;
datasource?: string;
xValue?: number;
yValue?: number;
}
export class StackedBarChart extends React.Component<StackedBarChartProps, StackedBarChartState> {
constructor(props: StackedBarChartProps) {
super(props);
this.state = {
width: this.props.svgWidth - this.props.margin.left - this.props.margin.right,
height: this.props.svgHeight - this.props.margin.bottom - this.props.margin.top,
};
}
componentWillReceiveProps(nextProps: StackedBarChartProps): void {
if (nextProps !== this.props) {
this.setState({
width: nextProps.svgWidth - this.props.margin.left - this.props.margin.right,
height: nextProps.svgHeight - this.props.margin.bottom - this.props.margin.top,
});
}
}
renderBarChart() {
const {
svgWidth,
svgHeight,
formatTick,
xScale,
yScale,
dataToRender,
changeActiveDatasource,
barWidth,
} = this.props;
const { width, height, hoverOn } = this.state;
return (
<div className={'bar-chart-container'}>
<svg
width={width}
height={height}
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
preserveAspectRatio={'xMinYMin meet'}
style={{ marginTop: '20px' }}
>
<ChartAxis
className={'gridline-x'}
transform={'translate(60, 0)'}
scale={d3
.axisLeft(yScale)
.ticks(5)
.tickSize(-width)
.tickFormat(() => '')
.tickSizeOuter(0)}
/>
<ChartAxis
className={'axis--x'}
transform={`translate(65, ${height})`}
scale={d3.axisBottom(xScale)}
/>
<ChartAxis
className={'axis--y'}
transform={'translate(60, 0)'}
scale={d3
.axisLeft(yScale)
.ticks(5)
.tickFormat((e: number) => formatTick(e))}
/>
<g className="bars-group" onMouseLeave={() => this.setState({ hoverOn: null })}>
<BarGroup
dataToRender={dataToRender}
changeActiveDatasource={changeActiveDatasource}
formatTick={formatTick}
xScale={xScale}
yScale={yScale}
onHoverBar={(e: HoveredBarInfo) => this.setState({ hoverOn: e })}
hoverOn={hoverOn}
barWidth={barWidth}
/>
{hoverOn && (
<g
className={'hovered-bar'}
onClick={() => {
this.setState({ hoverOn: null });
changeActiveDatasource(hoverOn.datasource as string);
}}
>
<rect
x={hoverOn.xCoordinate}
y={hoverOn.yCoordinate}
width={barWidth}
height={hoverOn.height}
/>
</g>
)}
</g>
</svg>
</div>
);
}
render(): JSX.Element {
const { activeDataType, formatTick } = this.props;
const { hoverOn } = this.state;
return (
<div className={'bar-chart'}>
<div className={'bar-chart-tooltip'}>
<div>Datasource: {hoverOn ? hoverOn.datasource : ''}</div>
<div>Time: {hoverOn ? hoverOn.xValue : ''}</div>
<div>
{`${activeDataType === 'countData' ? 'Count:' : 'Size:'} ${
hoverOn ? formatTick(hoverOn.yValue as number) : ''
}`}
</div>
</div>
{this.renderBarChart()}
</div>
);
}
}