| /** |
| * 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, { Component } from 'react'; |
| import { Chart, Axis, Tooltip, Geom } from 'bizcharts'; |
| import autoHeight from '../autoHeight'; |
| import styles from '../index.less'; |
| |
| const yTickOffset = 20; |
| |
| @autoHeight() |
| class HeatMap extends Component { |
| getScaleMap = maxResponseTimeOffset => { |
| const scaleMap = []; |
| const remainder = (maxResponseTimeOffset + 1) % (yTickOffset + 1); |
| const times = ~~((maxResponseTimeOffset + 1) / (yTickOffset + 1)); |
| if (remainder > 0) { |
| scaleMap.push([0, remainder - 1, times + 1]); |
| scaleMap.push([remainder, yTickOffset, times]); |
| } else { |
| scaleMap.push([0, yTickOffset, times]); |
| } |
| return scaleMap; |
| }; |
| |
| reduceData = (scaleMap, reducer, init = 0) => { |
| const result = []; |
| for (let i = 0; i < scaleMap.length; i += 1) { |
| const scale = scaleMap[i]; |
| for (let j = scale[0]; j <= scale[1]; j += 1) { |
| let item = init; |
| for (let k = 0; k < scale[2]; k += 1) { |
| item = reducer(item, k); |
| } |
| result.push(item); |
| } |
| } |
| return result; |
| }; |
| |
| mapXAxisData = (reducedData, datetime) => |
| reducedData.map((count, i) => ({ datetime, responseTime: i, count })); |
| |
| handlePlotClick = (dtStart, dtEnd, responseTime) => { |
| const removedUnit = responseTime.slice(0, responseTime.indexOf('ms')); |
| let min; |
| let max; |
| if (removedUnit.indexOf('>') === 0) { |
| min = parseInt(removedUnit.slice(1), 10); |
| } else { |
| const value = parseInt(removedUnit, 10); |
| min = value - 100 < 0 ? 0 : value - 100; |
| max = value; |
| } |
| this.props.onClick({ start: dtStart, end: dtEnd }, { min, max }); |
| }; |
| |
| render() { |
| const { |
| height, |
| data: { nodes, responseTimeStep }, |
| duration, |
| } = this.props; |
| |
| if (!nodes || nodes.length < 1) { |
| return <span style={{ display: 'none' }} />; |
| } |
| const { |
| display: { range }, |
| raw: { range: rawRange }, |
| } = duration; |
| |
| const source = []; |
| let maxResponseTimeOffset = 0; |
| for (let i = 0; i < nodes.length; i += 1) { |
| const item = nodes[i]; |
| if (item[0] >= range.length) { |
| break; |
| } |
| maxResponseTimeOffset = maxResponseTimeOffset > item[1] ? maxResponseTimeOffset : item[1]; |
| source.push({ |
| datetime: item[0], |
| responseTime: item[1], |
| count: item[2], |
| }); |
| } |
| const mergeSource = []; |
| let responseTimeAxis = []; |
| if (maxResponseTimeOffset > yTickOffset) { |
| const scaleMap = this.getScaleMap(maxResponseTimeOffset); |
| let data = 0; |
| const xAxisStepArray = this.reduceData(scaleMap, time => time + responseTimeStep); |
| responseTimeAxis = xAxisStepArray.map((_, i) => { |
| data += _; |
| return `${i === xAxisStepArray.length - 1 ? '>' : ''}${ |
| i === xAxisStepArray.length - 1 ? data - _ : data |
| }ms`; |
| }); |
| let datetime = 0; |
| while (source.length > 0) { |
| const reducedData = this.reduceData(scaleMap, count => { |
| const item = source.shift(); |
| return item ? item.count + count : count; |
| }); |
| mergeSource.push(...this.mapXAxisData(reducedData, datetime)); |
| datetime += 1; |
| } |
| } else { |
| for (let i = 0; i < maxResponseTimeOffset + 1; i += 1) { |
| responseTimeAxis.push( |
| `${ |
| i === maxResponseTimeOffset ? `>${i * responseTimeStep}` : (i + 1) * responseTimeStep |
| }ms` |
| ); |
| } |
| mergeSource.push(...source); |
| } |
| const cols = { |
| datetime: { |
| type: 'cat', |
| values: range, |
| tickCount: 5, |
| }, |
| responseTime: { |
| type: 'cat', |
| values: responseTimeAxis, |
| tickCount: 5, |
| }, |
| }; |
| return ( |
| <div className={styles.chart} style={{ height }}> |
| <div> |
| <Chart |
| data={mergeSource} |
| scale={cols} |
| forceFit |
| height={height * 1.4} |
| onPlotClick={({ |
| data: { |
| _origin: { datetime, responseTime }, |
| }, |
| }) => |
| this.handlePlotClick( |
| rawRange[datetime], |
| rawRange[datetime + 1], |
| responseTimeAxis[responseTime] |
| ) |
| } |
| > |
| <Axis |
| name="datetime" |
| grid={{ |
| align: 'center', |
| lineStyle: { |
| lineWidth: 1, |
| lineDash: null, |
| stroke: '#f0f0f0', |
| }, |
| showFirstLine: true, |
| }} |
| /> |
| <Axis |
| name="responseTime" |
| grid={{ |
| align: 'center', |
| lineStyle: { |
| lineWidth: 1, |
| lineDash: null, |
| stroke: '#f0f0f0', |
| }, |
| }} |
| /> |
| <Tooltip /> |
| <Geom |
| type="polygon" |
| position="datetime*responseTime" |
| color={['count', '#EBEDF0-#BAE7FF-#1890FF-#0050B3']} |
| style={{ stroke: '#fff', lineWidth: 1 }} |
| tooltip={[ |
| 'datetime*responseTime*count', |
| (datetime, responseTime, count) => { |
| return { |
| name: range[datetime], |
| title: `${responseTime > 0 ? responseTimeAxis[responseTime - 1] : 0}~${ |
| responseTimeAxis[responseTime] |
| }`, |
| value: count, |
| }; |
| }, |
| ]} |
| /> |
| </Chart> |
| </div> |
| </div> |
| ); |
| } |
| } |
| |
| export default HeatMap; |