Remove log viewer histogram code
parent
fe04ef3476
commit
757fd82ac5
|
@ -1,96 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {mount} from 'enzyme'
|
|
||||||
|
|
||||||
import HistogramChart from 'src/shared/components/HistogramChart'
|
|
||||||
import HistogramChartTooltip from 'src/shared/components/HistogramChartTooltip'
|
|
||||||
|
|
||||||
import {RemoteDataState} from 'src/types'
|
|
||||||
|
|
||||||
describe('HistogramChart', () => {
|
|
||||||
test('displays a HistogramChartSkeleton if empty data is passed', () => {
|
|
||||||
const props = {
|
|
||||||
data: [],
|
|
||||||
dataStatus: RemoteDataState.Done,
|
|
||||||
width: 600,
|
|
||||||
height: 400,
|
|
||||||
colorScale: () => 'blue',
|
|
||||||
colors: [],
|
|
||||||
sortBarGroups: (a, b) => a.value - b.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapper = mount(<HistogramChart {...props} />)
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('displays a nothing if passed width and height of 0', () => {
|
|
||||||
const props = {
|
|
||||||
data: [],
|
|
||||||
dataStatus: RemoteDataState.Done,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
colorScale: () => 'blue',
|
|
||||||
colors: [],
|
|
||||||
sortBarGroups: (a, b) => a.value - b.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapper = mount(<HistogramChart {...props} />)
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('displays the visualization with bars if nonempty data is passed', () => {
|
|
||||||
const props = {
|
|
||||||
data: [
|
|
||||||
{key: '0', time: 0, value: 0, group: 'a'},
|
|
||||||
{key: '1', time: 1, value: 1, group: 'a'},
|
|
||||||
{key: '2', time: 2, value: 2, group: 'b'},
|
|
||||||
],
|
|
||||||
dataStatus: RemoteDataState.Done,
|
|
||||||
width: 600,
|
|
||||||
height: 400,
|
|
||||||
colorScale: () => 'blue',
|
|
||||||
colors: [],
|
|
||||||
sortBarGroups: (a, b) => a.value - b.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapper = mount(<HistogramChart {...props} />)
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('displays a HistogramChartTooltip when hovering over bars', () => {
|
|
||||||
const props = {
|
|
||||||
data: [
|
|
||||||
{key: '0', time: 0, value: 0, group: 'a'},
|
|
||||||
{key: '1', time: 1, value: 1, group: 'a'},
|
|
||||||
{key: '2', time: 2, value: 2, group: 'b'},
|
|
||||||
],
|
|
||||||
dataStatus: RemoteDataState.Done,
|
|
||||||
width: 600,
|
|
||||||
height: 400,
|
|
||||||
colorScale: () => 'blue',
|
|
||||||
colors: [],
|
|
||||||
sortBarGroups: (a, b) => a.value - b.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapper = mount(<HistogramChart {...props} />)
|
|
||||||
|
|
||||||
const fakeMouseOverEvent = {
|
|
||||||
target: {
|
|
||||||
getBoundingClientRect() {
|
|
||||||
return {top: 10, right: 10, bottom: 5, left: 5}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper
|
|
||||||
.find('.histogram-chart-bars--bars')
|
|
||||||
.first()
|
|
||||||
.simulate('mouseover', fakeMouseOverEvent)
|
|
||||||
|
|
||||||
const tooltip = wrapper.find(HistogramChartTooltip)
|
|
||||||
|
|
||||||
expect(tooltip).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,214 +0,0 @@
|
||||||
import React, {PureComponent} from 'react'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import {scaleLinear, scaleTime, ScaleLinear, ScaleTime} from 'd3-scale'
|
|
||||||
|
|
||||||
import HistogramChartAxes from 'src/shared/components/HistogramChartAxes'
|
|
||||||
import HistogramChartBars from 'src/shared/components/HistogramChartBars'
|
|
||||||
import HistogramChartTooltip from 'src/shared/components/HistogramChartTooltip'
|
|
||||||
import HistogramChartSkeleton from 'src/shared/components/HistogramChartSkeleton'
|
|
||||||
|
|
||||||
import extentBy from 'src/utils/extentBy'
|
|
||||||
|
|
||||||
import {
|
|
||||||
HistogramData,
|
|
||||||
Margins,
|
|
||||||
HoverData,
|
|
||||||
ColorScale,
|
|
||||||
HistogramColor,
|
|
||||||
SortFn,
|
|
||||||
} from 'src/types/histogram'
|
|
||||||
|
|
||||||
const PADDING_TOP = 0.2
|
|
||||||
|
|
||||||
// Rather than use these magical constants, we could also render a digit and
|
|
||||||
// capture its measured width with as state before rendering anything else.
|
|
||||||
// Doing so would be robust but overkill.
|
|
||||||
const DIGIT_WIDTH = 7
|
|
||||||
const PERIOD_DIGIT_WIDTH = 4
|
|
||||||
|
|
||||||
interface RenderPropArgs {
|
|
||||||
xScale: ScaleTime<number, number>
|
|
||||||
yScale: ScaleLinear<number, number>
|
|
||||||
adjustedWidth: number
|
|
||||||
adjustedHeight: number
|
|
||||||
margins: Margins
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: HistogramData
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
colors: HistogramColor[]
|
|
||||||
colorScale: ColorScale
|
|
||||||
onBarClick?: (time: string) => void
|
|
||||||
sortBarGroups: SortFn
|
|
||||||
children?: (args: RenderPropArgs) => JSX.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
hoverData?: HoverData
|
|
||||||
}
|
|
||||||
|
|
||||||
class HistogramChart extends PureComponent<Props, State> {
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
data,
|
|
||||||
colorScale,
|
|
||||||
colors,
|
|
||||||
onBarClick,
|
|
||||||
sortBarGroups,
|
|
||||||
children,
|
|
||||||
} = this.props
|
|
||||||
const {margins} = this
|
|
||||||
|
|
||||||
if (width === 0 || height === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.length) {
|
|
||||||
return (
|
|
||||||
<HistogramChartSkeleton
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
margins={margins}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {hoverData} = this.state
|
|
||||||
const {xScale, yScale, adjustedWidth, adjustedHeight, bodyTransform} = this
|
|
||||||
|
|
||||||
const renderPropArgs = {
|
|
||||||
xScale,
|
|
||||||
yScale,
|
|
||||||
adjustedWidth,
|
|
||||||
adjustedHeight,
|
|
||||||
margins,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="histogram-chart">
|
|
||||||
<svg width={width} height={height} className="histogram-chart">
|
|
||||||
<defs>
|
|
||||||
<clipPath id="histogram-chart--bars-clip">
|
|
||||||
<rect x="0" y="0" width={adjustedWidth} height={adjustedHeight} />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<g className="histogram-chart--axes">
|
|
||||||
<HistogramChartAxes
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
margins={margins}
|
|
||||||
xScale={xScale}
|
|
||||||
yScale={yScale}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform={bodyTransform}
|
|
||||||
className="histogram-chart--bars"
|
|
||||||
clipPath="url(#histogram-chart--bars-clip)"
|
|
||||||
>
|
|
||||||
<HistogramChartBars
|
|
||||||
width={adjustedWidth}
|
|
||||||
height={adjustedHeight}
|
|
||||||
data={data}
|
|
||||||
xScale={xScale}
|
|
||||||
yScale={yScale}
|
|
||||||
colorScale={colorScale}
|
|
||||||
hoverData={hoverData}
|
|
||||||
onHover={this.handleHover}
|
|
||||||
colors={colors}
|
|
||||||
onBarClick={onBarClick}
|
|
||||||
sortBarGroups={sortBarGroups}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<div className="histogram-chart--overlays">
|
|
||||||
{!!children && children(renderPropArgs)}
|
|
||||||
{hoverData && (
|
|
||||||
<HistogramChartTooltip
|
|
||||||
data={hoverData}
|
|
||||||
colorScale={colorScale}
|
|
||||||
colors={colors}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get xScale(): ScaleTime<number, number> {
|
|
||||||
const {adjustedWidth} = this
|
|
||||||
const {data} = this.props
|
|
||||||
|
|
||||||
const [t0, t1] = extentBy(data, d => d.time)
|
|
||||||
|
|
||||||
return scaleTime()
|
|
||||||
.domain([new Date(t0.time), new Date(t1.time)])
|
|
||||||
.range([0, adjustedWidth])
|
|
||||||
}
|
|
||||||
|
|
||||||
private get yScale(): ScaleLinear<number, number> {
|
|
||||||
const {adjustedHeight, maxAggregateCount} = this
|
|
||||||
|
|
||||||
return scaleLinear()
|
|
||||||
.domain([0, maxAggregateCount + PADDING_TOP * maxAggregateCount])
|
|
||||||
.range([adjustedHeight, 0])
|
|
||||||
}
|
|
||||||
|
|
||||||
private get adjustedWidth(): number {
|
|
||||||
const {margins} = this
|
|
||||||
|
|
||||||
return this.props.width - margins.left - margins.right
|
|
||||||
}
|
|
||||||
|
|
||||||
private get adjustedHeight(): number {
|
|
||||||
const {margins} = this
|
|
||||||
|
|
||||||
return this.props.height - margins.top - margins.bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
private get bodyTransform(): string {
|
|
||||||
const {margins} = this
|
|
||||||
|
|
||||||
return `translate(${margins.left}, ${margins.top})`
|
|
||||||
}
|
|
||||||
|
|
||||||
private get margins(): Margins {
|
|
||||||
const {maxAggregateCount} = this
|
|
||||||
|
|
||||||
const domainTop = maxAggregateCount + PADDING_TOP * maxAggregateCount
|
|
||||||
const left = domainTop.toString().length * DIGIT_WIDTH + PERIOD_DIGIT_WIDTH
|
|
||||||
|
|
||||||
return {top: 5, right: 0, bottom: 20, left}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get maxAggregateCount(): number {
|
|
||||||
const {data} = this.props
|
|
||||||
|
|
||||||
if (!data.length) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups = _.groupBy(data, 'time')
|
|
||||||
const counts = Object.values(groups).map(group =>
|
|
||||||
group.reduce((sum, current) => sum + current.value, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Math.max(...counts)
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleHover = (hoverData: HoverData): void => {
|
|
||||||
this.setState({hoverData})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HistogramChart
|
|
|
@ -1,92 +0,0 @@
|
||||||
import React, {PureComponent} from 'react'
|
|
||||||
import {ScaleLinear, ScaleTime} from 'd3-scale'
|
|
||||||
|
|
||||||
import {Margins} from 'src/types/histogram'
|
|
||||||
|
|
||||||
const Y_TICK_COUNT = 5
|
|
||||||
const Y_TICK_PADDING_RIGHT = 7
|
|
||||||
const X_TICK_COUNT = 10
|
|
||||||
const X_TICK_PADDING_TOP = 8
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
margins: Margins
|
|
||||||
xScale: ScaleTime<number, number>
|
|
||||||
yScale: ScaleLinear<number, number>
|
|
||||||
}
|
|
||||||
|
|
||||||
class HistogramChartAxes extends PureComponent<Props> {
|
|
||||||
public render() {
|
|
||||||
const {xTickData, yTickData} = this
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.renderYTicks(yTickData)}
|
|
||||||
{this.renderYLabels(yTickData)}
|
|
||||||
{this.renderXLabels(xTickData)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderYTicks(yTickData) {
|
|
||||||
return yTickData.map(({x1, x2, y, key}) => (
|
|
||||||
<line className="y-tick" key={key} x1={x1} x2={x2} y1={y} y2={y} />
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderYLabels(yTickData) {
|
|
||||||
return yTickData.map(({x1, y, label, key}) => (
|
|
||||||
<text className="y-label" key={key} x={x1 - Y_TICK_PADDING_RIGHT} y={y}>
|
|
||||||
{label}
|
|
||||||
</text>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderXLabels(xTickData) {
|
|
||||||
return xTickData.map(({x, y, label, key}) => (
|
|
||||||
<text className="x-label" key={key} y={y} x={x}>
|
|
||||||
{label}
|
|
||||||
</text>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
private get xTickData() {
|
|
||||||
const {margins, xScale, width, height} = this.props
|
|
||||||
|
|
||||||
const y = height - margins.bottom + X_TICK_PADDING_TOP
|
|
||||||
const formatTime = xScale.tickFormat()
|
|
||||||
|
|
||||||
return xScale
|
|
||||||
.ticks(X_TICK_COUNT)
|
|
||||||
.filter(val => {
|
|
||||||
const x = xScale(val)
|
|
||||||
|
|
||||||
// Don't render labels that will be cut off
|
|
||||||
return x > margins.left && x < width - margins.right
|
|
||||||
})
|
|
||||||
.map(val => {
|
|
||||||
const x = xScale(val)
|
|
||||||
const label = formatTime(val)
|
|
||||||
const key = `${label}-${x}-${y}`
|
|
||||||
|
|
||||||
return {label, x, y, key}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private get yTickData() {
|
|
||||||
const {width, margins, yScale} = this.props
|
|
||||||
|
|
||||||
return yScale.ticks(Y_TICK_COUNT).map(val => {
|
|
||||||
const label = val
|
|
||||||
const x1 = margins.left
|
|
||||||
const x2 = margins.left + width
|
|
||||||
const y = margins.top + yScale(val)
|
|
||||||
const key = `${label}-${x1}-${x2}-${y}`
|
|
||||||
|
|
||||||
return {label, x1, x2, y, key}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HistogramChartAxes
|
|
|
@ -1,261 +0,0 @@
|
||||||
import React, {PureComponent, MouseEvent} from 'react'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import {ScaleLinear, ScaleTime} from 'd3-scale'
|
|
||||||
import {color} from 'd3-color'
|
|
||||||
|
|
||||||
import {getDeep} from 'src/utils/wrappers'
|
|
||||||
|
|
||||||
import {
|
|
||||||
HistogramData,
|
|
||||||
HistogramDatum,
|
|
||||||
HoverData,
|
|
||||||
TooltipAnchor,
|
|
||||||
ColorScale,
|
|
||||||
HistogramColor,
|
|
||||||
SortFn,
|
|
||||||
} from 'src/types/histogram'
|
|
||||||
|
|
||||||
const BAR_BORDER_RADIUS = 3
|
|
||||||
const BAR_PADDING_SIDES = 4
|
|
||||||
const HOVER_BRIGTHEN_FACTOR = 0.4
|
|
||||||
const TOOLTIP_HORIZONTAL_MARGIN = 5
|
|
||||||
const TOOLTIP_REFLECT_DIST = 100
|
|
||||||
|
|
||||||
const getBarWidth = ({data, xScale, width}): number => {
|
|
||||||
const dataInView = data.filter(
|
|
||||||
d => xScale(d.time) >= 0 && xScale(d.time) <= width
|
|
||||||
)
|
|
||||||
const barCount = Object.values(_.groupBy(dataInView, 'time')).length
|
|
||||||
|
|
||||||
return Math.round(width / barCount - BAR_PADDING_SIDES)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BarGroup {
|
|
||||||
key: string
|
|
||||||
clip: {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
bars: Array<{
|
|
||||||
key: string
|
|
||||||
group: string
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
fill: string
|
|
||||||
}>
|
|
||||||
data: HistogramData
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBarGroups = ({
|
|
||||||
data,
|
|
||||||
width,
|
|
||||||
xScale,
|
|
||||||
yScale,
|
|
||||||
colorScale,
|
|
||||||
hoverData,
|
|
||||||
colors,
|
|
||||||
sortBarGroups,
|
|
||||||
}: Partial<Props>): BarGroup[] => {
|
|
||||||
const barWidth = getBarWidth({data, xScale, width})
|
|
||||||
const visibleData = data.filter(d => d.value !== 0)
|
|
||||||
const timeGroups = Object.values(_.groupBy(visibleData, 'time'))
|
|
||||||
|
|
||||||
for (const timeGroup of timeGroups) {
|
|
||||||
timeGroup.sort(sortBarGroups)
|
|
||||||
}
|
|
||||||
|
|
||||||
let hoverDataKeys = []
|
|
||||||
|
|
||||||
if (!!hoverData) {
|
|
||||||
hoverDataKeys = hoverData.data.map(h => h.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeGroups.map(timeGroup => {
|
|
||||||
const time = timeGroup[0].time
|
|
||||||
const x = xScale(time) - barWidth / 2
|
|
||||||
const total = _.sumBy(timeGroup, 'value')
|
|
||||||
|
|
||||||
const barGroup = {
|
|
||||||
key: `${time}-${total}-${x}`,
|
|
||||||
clip: {
|
|
||||||
x,
|
|
||||||
y: yScale(total),
|
|
||||||
width: barWidth,
|
|
||||||
height: yScale(0) - yScale(total) + BAR_BORDER_RADIUS,
|
|
||||||
},
|
|
||||||
bars: [],
|
|
||||||
data: timeGroup,
|
|
||||||
}
|
|
||||||
|
|
||||||
let offset = 0
|
|
||||||
|
|
||||||
timeGroup.forEach((d: HistogramDatum) => {
|
|
||||||
const height = yScale(0) - yScale(d.value)
|
|
||||||
const k = hoverDataKeys.includes(d.key) ? HOVER_BRIGTHEN_FACTOR : 0
|
|
||||||
const groupColor = colors.find(c => c.group === d.group)
|
|
||||||
const fill = color(colorScale(_.get(groupColor, 'color', ''), d.group))
|
|
||||||
.brighter(k)
|
|
||||||
.hex()
|
|
||||||
|
|
||||||
barGroup.bars.push({
|
|
||||||
key: d.key,
|
|
||||||
group: d.group,
|
|
||||||
x,
|
|
||||||
y: yScale(d.value) - offset,
|
|
||||||
width: barWidth,
|
|
||||||
height,
|
|
||||||
fill,
|
|
||||||
})
|
|
||||||
|
|
||||||
offset += height
|
|
||||||
})
|
|
||||||
|
|
||||||
return barGroup
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BarGroup {
|
|
||||||
key: string
|
|
||||||
clip: {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
bars: Array<{
|
|
||||||
key: string
|
|
||||||
group: string
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
fill: string
|
|
||||||
}>
|
|
||||||
data: HistogramData
|
|
||||||
}
|
|
||||||
interface Props {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
data: HistogramData
|
|
||||||
xScale: ScaleTime<number, number>
|
|
||||||
yScale: ScaleLinear<number, number>
|
|
||||||
colorScale: ColorScale
|
|
||||||
hoverData?: HoverData
|
|
||||||
colors: HistogramColor[]
|
|
||||||
onHover: (h: HoverData) => void
|
|
||||||
onBarClick?: (time: string) => void
|
|
||||||
sortBarGroups: SortFn
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
barGroups: BarGroup[]
|
|
||||||
}
|
|
||||||
|
|
||||||
class HistogramChartBars extends PureComponent<Props, State> {
|
|
||||||
public static getDerivedStateFromProps(props: Props) {
|
|
||||||
return {barGroups: getBarGroups(props)}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {barGroups: []}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const {barGroups} = this.state
|
|
||||||
|
|
||||||
return barGroups.map(group => {
|
|
||||||
const {key, clip, bars} = group
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g
|
|
||||||
key={key}
|
|
||||||
className="histogram-chart-bars--bars"
|
|
||||||
data-key={key}
|
|
||||||
onMouseOver={this.handleMouseOver}
|
|
||||||
onMouseOut={this.handleMouseOut}
|
|
||||||
onClick={this.handleBarClick(group.data)}
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<clipPath id={`histogram-chart-bars--clip-${key}`}>
|
|
||||||
<rect
|
|
||||||
x={clip.x}
|
|
||||||
y={clip.y}
|
|
||||||
width={clip.width}
|
|
||||||
height={clip.height}
|
|
||||||
rx={BAR_BORDER_RADIUS}
|
|
||||||
ry={BAR_BORDER_RADIUS}
|
|
||||||
/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
{bars.map(d => (
|
|
||||||
<rect
|
|
||||||
key={d.key}
|
|
||||||
className="histogram-chart-bars--bar"
|
|
||||||
x={d.x}
|
|
||||||
y={d.y}
|
|
||||||
width={d.width}
|
|
||||||
height={d.height}
|
|
||||||
fill={d.fill}
|
|
||||||
clipPath={`url(#histogram-chart-bars--clip-${key})`}
|
|
||||||
data-group={d.group}
|
|
||||||
data-key={d.key}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</g>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleBarClick = data => (): void => {
|
|
||||||
const {onBarClick} = this.props
|
|
||||||
|
|
||||||
if (onBarClick) {
|
|
||||||
const time = data[0].time
|
|
||||||
onBarClick(time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMouseOver = (e: MouseEvent<SVGGElement>): void => {
|
|
||||||
const groupKey = getDeep<string>(e, 'currentTarget.dataset.key', '')
|
|
||||||
|
|
||||||
if (!groupKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const {barGroups} = this.state
|
|
||||||
const hoverGroup = barGroups.find(d => d.key === groupKey)
|
|
||||||
|
|
||||||
if (!hoverGroup) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = _.get(hoverGroup, 'data').reverse()
|
|
||||||
const barGroup = e.currentTarget as SVGGElement
|
|
||||||
const boundingRect = barGroup.getBoundingClientRect()
|
|
||||||
const boundingRectHeight = boundingRect.bottom - boundingRect.top
|
|
||||||
const y = boundingRect.top + boundingRectHeight / 2
|
|
||||||
|
|
||||||
let x = boundingRect.right + TOOLTIP_HORIZONTAL_MARGIN
|
|
||||||
let anchor: TooltipAnchor = 'left'
|
|
||||||
|
|
||||||
// This makes an assumption that the component is within the viewport
|
|
||||||
if (x >= window.innerWidth - TOOLTIP_REFLECT_DIST) {
|
|
||||||
x = window.innerWidth - boundingRect.left + TOOLTIP_HORIZONTAL_MARGIN
|
|
||||||
anchor = 'right'
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onHover({data, x, y, anchor})
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMouseOut = (): void => {
|
|
||||||
this.props.onHover(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HistogramChartBars
|
|
|
@ -1,37 +0,0 @@
|
||||||
import React, {SFC} from 'react'
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
import {Margins} from 'src/types/histogram'
|
|
||||||
|
|
||||||
const NUM_TICKS = 5
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
margins: Margins
|
|
||||||
}
|
|
||||||
|
|
||||||
const HistogramChartSkeleton: SFC<Props> = props => {
|
|
||||||
const {margins, width, height} = props
|
|
||||||
|
|
||||||
const spacing = (height - margins.top - margins.bottom) / NUM_TICKS
|
|
||||||
const y1 = height - margins.bottom
|
|
||||||
const tickYs = _.range(0, NUM_TICKS).map(i => y1 - i * spacing)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg className="histogram-chart-skeleton" width={width} height={height}>
|
|
||||||
{tickYs.map((y, i) => (
|
|
||||||
<line
|
|
||||||
key={i}
|
|
||||||
className="y-tick"
|
|
||||||
x1={margins.left}
|
|
||||||
x2={width - margins.right}
|
|
||||||
y1={y}
|
|
||||||
y2={y}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HistogramChartSkeleton
|
|
|
@ -1,63 +0,0 @@
|
||||||
import React, {SFC, CSSProperties} from 'react'
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
import {HoverData, ColorScale, HistogramColor} from 'src/types/histogram'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: HoverData
|
|
||||||
colorScale: ColorScale
|
|
||||||
colors: HistogramColor[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const HistogramChartTooltip: SFC<Props> = props => {
|
|
||||||
const {colorScale, colors} = props
|
|
||||||
const {data, x, y, anchor = 'left'} = props.data
|
|
||||||
|
|
||||||
const tooltipStyle: CSSProperties = {
|
|
||||||
position: 'fixed',
|
|
||||||
top: y,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anchor === 'left') {
|
|
||||||
tooltipStyle.left = x
|
|
||||||
} else {
|
|
||||||
tooltipStyle.right = x
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="histogram-chart-tooltip" style={tooltipStyle}>
|
|
||||||
<div className="histogram-chart-tooltip--column">
|
|
||||||
{data.map(d => {
|
|
||||||
const groupColor = colors.find(c => c.group === d.group)
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={d.key}
|
|
||||||
style={{
|
|
||||||
color: colorScale(_.get(groupColor, 'color', ''), d.group),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{d.value}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="histogram-chart-tooltip--column">
|
|
||||||
{data.map(d => {
|
|
||||||
const groupColor = colors.find(c => c.group === d.group)
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={d.key}
|
|
||||||
style={{
|
|
||||||
color: colorScale(_.get(groupColor, 'color', ''), d.group),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{d.group}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HistogramChartTooltip
|
|
|
@ -1,430 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`HistogramChart displays a HistogramChartSkeleton if empty data is passed 1`] = `
|
|
||||||
<HistogramChart
|
|
||||||
colorScale={[Function]}
|
|
||||||
colors={Array []}
|
|
||||||
data={Array []}
|
|
||||||
dataStatus="Done"
|
|
||||||
height={400}
|
|
||||||
sortBarGroups={[Function]}
|
|
||||||
width={600}
|
|
||||||
>
|
|
||||||
<HistogramChartSkeleton
|
|
||||||
height={400}
|
|
||||||
margins={
|
|
||||||
Object {
|
|
||||||
"bottom": 20,
|
|
||||||
"left": 11,
|
|
||||||
"right": 0,
|
|
||||||
"top": 5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width={600}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="histogram-chart-skeleton"
|
|
||||||
height={400}
|
|
||||||
width={600}
|
|
||||||
>
|
|
||||||
<line
|
|
||||||
className="y-tick"
|
|
||||||
key="0"
|
|
||||||
x1={11}
|
|
||||||
x2={600}
|
|
||||||
y1={380}
|
|
||||||
y2={380}
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
className="y-tick"
|
|
||||||
key="1"
|
|
||||||
x1={11}
|
|
||||||
x2={600}
|
|
||||||
y1={305}
|
|
||||||
y2={305}
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
className="y-tick"
|
|
||||||
key="2"
|
|
||||||
x1={11}
|
|
||||||
x2={600}
|
|
||||||
y1={230}
|
|
||||||
y2={230}
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
className="y-tick"
|
|
||||||
key="3"
|
|
||||||
x1={11}
|
|
||||||
x2={600}
|
|
||||||
y1={155}
|
|
||||||
y2={155}
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
className="y-tick"
|
|
||||||
key="4"
|
|
||||||
x1={11}
|
|
||||||
x2={600}
|
|
||||||
y1={80}
|
|
||||||
y2={80}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</HistogramChartSkeleton>
|
|
||||||
</HistogramChart>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`HistogramChart displays a HistogramChartTooltip when hovering over bars 1`] = `
|
|
||||||
<HistogramChartTooltip
|
|
||||||
colorScale={[Function]}
|
|
||||||
colors={Array []}
|
|
||||||
data={
|
|
||||||
Object {
|
|
||||||
"anchor": "left",
|
|
||||||
"data": Array [
|
|
||||||
Object {
|
|
||||||
"group": "a",
|
|
||||||
"key": "1",
|
|
||||||
"time": 1,
|
|
||||||
"value": 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"x": 5,
|
|
||||||
"y": 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="histogram-chart-tooltip"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"left": 5,
|
|
||||||
"position": "fixed",
|
|
||||||
"top": 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="histogram-chart-tooltip--column"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
key="1"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"color": "blue",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
1
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="histogram-chart-tooltip--column"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
key="1"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"color": "blue",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
a
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</HistogramChartTooltip>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`HistogramChart displays a nothing if passed width and height of 0 1`] = `
|
|
||||||
<HistogramChart
|
|
||||||
colorScale={[Function]}
|
|
||||||
colors={Array []}
|
|
||||||
data={Array []}
|
|
||||||
dataStatus="Done"
|
|
||||||
height={0}
|
|
||||||
sortBarGroups={[Function]}
|
|
||||||
width={0}
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`HistogramChart displays the visualization with bars if nonempty data is passed 1`] = `
|
|
||||||
<HistogramChart
|
|
||||||
colorScale={[Function]}
|
|
||||||
colors={Array []}
|
|
||||||
data={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"group": "a",
|
|
||||||
"key": "0",
|
|
||||||
"time": 0,
|
|
||||||
"value": 0,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"group": "a",
|
|
||||||
"key": "1",
|
|
||||||
"time": 1,
|
|
||||||
"value": 1,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"group": "b",
|
|
||||||
"key": "2",
|
|
||||||
"time": 2,
|
|
||||||
"value": 2,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
dataStatus="Done"
|
|
||||||
height={400}
|
|
||||||
sortBarGroups={[Function]}
|
|
||||||
width={600}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="histogram-chart"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="histogram-chart"
|
|
||||||
height={400}
|
|
||||||
width={600}
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<clipPath
|
|
||||||
id="histogram-chart--bars-clip"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
height={375}
|
|
||||||
width={575}
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
className="histogram-chart--axes"
|
|
||||||
>
|
|
||||||
<HistogramChartAxes
|
|
||||||
height={400}
|
|
||||||
margins={
|
|
||||||
Object {
|
|
||||||
"bottom": 20,
|
|
||||||
"left": 25,
|
|
||||||
"right": 0,
|
|
||||||
"top": 5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width={600}
|
|
||||||
xScale={[Function]}
|
|
||||||
yScale={[Function]}
|
|
||||||
>
|
|
||||||
<line
|
|
||||||
className="y-tick"
|
|
||||||
key="0-25-625-380"
|
|
||||||
x1={25}
|
|
||||||
x2={625}
|
|
||||||
y1={380}
|
|
||||||
y2={380}
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
className="y-tick"
|
|
||||||
key="0.5-25-625-301.875"
|
|
||||||
x1={25}
|
|
||||||
x2={625}
|
|
||||||
y1={301.875}
|
|
||||||
y2={301.875}
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
className="y-tick"
|
|
||||||
key="1-25-625-223.75"
|
|
||||||
x1={25}
|
|
||||||
x2={625}
|
|
||||||
y1={223.75}
|
|
||||||
y2={223.75}
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
className="y-tick"
|
|
||||||
key="1.5-25-625-145.625"
|
|
||||||
x1={25}
|
|
||||||
x2={625}
|
|
||||||
y1={145.625}
|
|
||||||
y2={145.625}
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
className="y-tick"
|
|
||||||
key="2-25-625-67.5"
|
|
||||||
x1={25}
|
|
||||||
x2={625}
|
|
||||||
y1={67.5}
|
|
||||||
y2={67.5}
|
|
||||||
/>
|
|
||||||
<text
|
|
||||||
className="y-label"
|
|
||||||
key="0-25-625-380"
|
|
||||||
x={18}
|
|
||||||
y={380}
|
|
||||||
>
|
|
||||||
0
|
|
||||||
</text>
|
|
||||||
<text
|
|
||||||
className="y-label"
|
|
||||||
key="0.5-25-625-301.875"
|
|
||||||
x={18}
|
|
||||||
y={301.875}
|
|
||||||
>
|
|
||||||
0.5
|
|
||||||
</text>
|
|
||||||
<text
|
|
||||||
className="y-label"
|
|
||||||
key="1-25-625-223.75"
|
|
||||||
x={18}
|
|
||||||
y={223.75}
|
|
||||||
>
|
|
||||||
1
|
|
||||||
</text>
|
|
||||||
<text
|
|
||||||
className="y-label"
|
|
||||||
key="1.5-25-625-145.625"
|
|
||||||
x={18}
|
|
||||||
y={145.625}
|
|
||||||
>
|
|
||||||
1.5
|
|
||||||
</text>
|
|
||||||
<text
|
|
||||||
className="y-label"
|
|
||||||
key="2-25-625-67.5"
|
|
||||||
x={18}
|
|
||||||
y={67.5}
|
|
||||||
>
|
|
||||||
2
|
|
||||||
</text>
|
|
||||||
<text
|
|
||||||
className="x-label"
|
|
||||||
key=".001-287.5-388"
|
|
||||||
x={287.5}
|
|
||||||
y={388}
|
|
||||||
>
|
|
||||||
.001
|
|
||||||
</text>
|
|
||||||
<text
|
|
||||||
className="x-label"
|
|
||||||
key=".002-575-388"
|
|
||||||
x={575}
|
|
||||||
y={388}
|
|
||||||
>
|
|
||||||
.002
|
|
||||||
</text>
|
|
||||||
</HistogramChartAxes>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
className="histogram-chart--bars"
|
|
||||||
clipPath="url(#histogram-chart--bars-clip)"
|
|
||||||
transform="translate(25, 5)"
|
|
||||||
>
|
|
||||||
<HistogramChartBars
|
|
||||||
colorScale={[Function]}
|
|
||||||
colors={Array []}
|
|
||||||
data={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"group": "a",
|
|
||||||
"key": "0",
|
|
||||||
"time": 0,
|
|
||||||
"value": 0,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"group": "a",
|
|
||||||
"key": "1",
|
|
||||||
"time": 1,
|
|
||||||
"value": 1,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"group": "b",
|
|
||||||
"key": "2",
|
|
||||||
"time": 2,
|
|
||||||
"value": 2,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
height={375}
|
|
||||||
onHover={[Function]}
|
|
||||||
sortBarGroups={[Function]}
|
|
||||||
width={575}
|
|
||||||
xScale={[Function]}
|
|
||||||
yScale={[Function]}
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
className="histogram-chart-bars--bars"
|
|
||||||
data-key="1-1-193.5"
|
|
||||||
key="1-1-193.5"
|
|
||||||
onClick={[Function]}
|
|
||||||
onMouseOut={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<clipPath
|
|
||||||
id="histogram-chart-bars--clip-1-1-193.5"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
height={159.25}
|
|
||||||
rx={3}
|
|
||||||
ry={3}
|
|
||||||
width={188}
|
|
||||||
x={193.5}
|
|
||||||
y={218.75}
|
|
||||||
/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<rect
|
|
||||||
className="histogram-chart-bars--bar"
|
|
||||||
clipPath="url(#histogram-chart-bars--clip-1-1-193.5)"
|
|
||||||
data-group="a"
|
|
||||||
data-key="1"
|
|
||||||
fill="#0000ff"
|
|
||||||
height={156.25}
|
|
||||||
key="1"
|
|
||||||
width={188}
|
|
||||||
x={193.5}
|
|
||||||
y={218.75}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
className="histogram-chart-bars--bars"
|
|
||||||
data-key="2-2-481"
|
|
||||||
key="2-2-481"
|
|
||||||
onClick={[Function]}
|
|
||||||
onMouseOut={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<clipPath
|
|
||||||
id="histogram-chart-bars--clip-2-2-481"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
height={315.5}
|
|
||||||
rx={3}
|
|
||||||
ry={3}
|
|
||||||
width={188}
|
|
||||||
x={481}
|
|
||||||
y={62.5}
|
|
||||||
/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<rect
|
|
||||||
className="histogram-chart-bars--bar"
|
|
||||||
clipPath="url(#histogram-chart-bars--clip-2-2-481)"
|
|
||||||
data-group="b"
|
|
||||||
data-key="2"
|
|
||||||
fill="#0000ff"
|
|
||||||
height={312.5}
|
|
||||||
key="2"
|
|
||||||
width={188}
|
|
||||||
x={481}
|
|
||||||
y={62.5}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</HistogramChartBars>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<div
|
|
||||||
className="histogram-chart--overlays"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</HistogramChart>
|
|
||||||
`;
|
|
Loading…
Reference in New Issue