Remove log viewer histogram code

pull/12348/head
Christopher Henn 2019-03-04 17:00:55 -08:00 committed by Chris Henn
parent fe04ef3476
commit 757fd82ac5
7 changed files with 0 additions and 1193 deletions

View File

@ -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()
})
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>
`;