Skip to content

Commit

Permalink
Implement histogram visualization type
Browse files Browse the repository at this point in the history
  • Loading branch information
chnn committed Feb 12, 2019
1 parent 4ae161d commit 93ccd1d
Show file tree
Hide file tree
Showing 31 changed files with 1,937 additions and 17 deletions.
28 changes: 14 additions & 14 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"classnames": "^2.2.3",
"codemirror": "^5.36.0",
"d3-color": "^1.2.0",
"d3-format": "^1.3.2",
"d3-scale": "^2.1.0",
"dygraphs": "2.1.0",
"encoding-down": "^5.0.4",
Expand Down
97 changes: 97 additions & 0 deletions ui/src/minard/components/Axes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, {useRef, useLayoutEffect, SFC} from 'react'

import {PlotEnv, TICK_PADDING_RIGHT, TICK_PADDING_TOP} from 'src/minard'
import {clearCanvas} from 'src/minard/utils/clearCanvas'

interface Props {
env: PlotEnv
axesStroke?: string
tickFont?: string
tickFill?: string
}

export const drawAxes = (
canvas: HTMLCanvasElement,
env: PlotEnv,
axesStroke: string,
tickFont: string,
tickFill: string
) => {
const {
width,
height,
margins,
xTicks,
yTicks,
defaults: {
scales: {x: xScale, y: yScale},
},
} = env

clearCanvas(canvas, width, height)

const context = canvas.getContext('2d')
const xAxisY = height - margins.bottom

// Draw x axis line
context.strokeStyle = axesStroke
context.beginPath()
context.moveTo(margins.left, xAxisY)
context.lineTo(width - margins.right, xAxisY)
context.stroke()

// Draw y axis line
context.beginPath()
context.moveTo(margins.left, xAxisY)
context.lineTo(margins.left, margins.top)
context.stroke()

context.font = tickFont
context.fillStyle = tickFill
context.textAlign = 'center'
context.textBaseline = 'top'

// Draw and label each tick on the x axis
for (const xTick of xTicks) {
const x = xScale(xTick) + margins.left

context.beginPath()
context.moveTo(x, xAxisY)
context.lineTo(x, margins.top)
context.stroke()

context.fillText(xTick, x, xAxisY + TICK_PADDING_TOP)
}

context.textAlign = 'end'
context.textBaseline = 'middle'

// Draw and label each tick on the y axis
for (const yTick of yTicks) {
const y = yScale(yTick) + margins.top

context.beginPath()
context.moveTo(margins.left, y)
context.lineTo(width - margins.right, y)
context.stroke()

context.fillText(yTick, margins.left - TICK_PADDING_RIGHT, y)
}
}

export const Axes: SFC<Props> = props => {
const {children, env, tickFill, tickFont, axesStroke} = props
const canvas = useRef<HTMLCanvasElement>(null)

useLayoutEffect(
() => drawAxes(canvas.current, env, axesStroke, tickFont, tickFill),
[canvas.current, env, axesStroke, tickFont, tickFill]
)

return (
<>
{children}
<canvas className="minard-axes" ref={canvas} />
</>
)
}
147 changes: 147 additions & 0 deletions ui/src/minard/components/Histogram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, {useState, useEffect, SFC} from 'react'
import uuid from 'uuid'

import {PlotEnv} from 'src/minard'
import * as stats from 'src/minard/utils/stats'
import {assert} from 'src/minard/utils/assert'
import {registerLayer, unregisterLayer} from 'src/minard/utils/plotEnvActions'
import HistogramBars from 'src/minard/components/HistogramBars'
import HistogramTooltip from 'src/minard/components/HistogramTooltip'
import {findHoveredRowIndices} from 'src/minard/utils/findHoveredRowIndices'

export enum Position {
Stacked = 'stacked',
Overlaid = 'overlaid',
}

export interface Props {
env: PlotEnv
x?: string
fill?: string
position?: Position
bins?: number
colors?: string[]
tooltip?: (props: TooltipProps) => JSX.Element
}

export interface TooltipProps {
x: string
fill: string
xMin: number
xMax: number
counts: Array<{fill: string | number | boolean; count: number; color: string}>
}

export const Histogram: SFC<Props> = props => {
const [layerKey] = useState(() => uuid.v4())

const {bins, position} = props
const {layers, defaults, dispatch} = props.env
const layer = layers[layerKey]
const table = defaults.table
const x = props.x || defaults.aesthetics.x
const fill = props.fill || defaults.aesthetics.fill
const colors = props.colors

useEffect(
() => {
const xCol = table.columns[x]
const xColType = table.columnTypes[x]
const fillCol = table.columns[fill]
const fillColType = table.columnTypes[fill]

assert('expected an `x` aesthetic', !!x)
assert(`table does not contain column "${x}"`, !!xCol)

const [statTable, mappings] = stats.bin(
xCol,
xColType,
fillCol,
fillColType,
bins,
position
)

dispatch(registerLayer(layerKey, statTable, mappings, colors))

return () => dispatch(unregisterLayer(layerKey))
},
[table, x, fill, position, bins, colors]
)

if (!layer) {
return null
}

const {
innerWidth,
innerHeight,
defaults: {
scales: {x: xScale, y: yScale, fill: layerFillScale},
},
} = props.env

const {
aesthetics,
table: {columns},
scales: {fill: defaultFillScale},
} = layer

const fillScale = layerFillScale || defaultFillScale
const xMinCol = columns[aesthetics.xMin]
const xMaxCol = columns[aesthetics.xMax]
const yMinCol = columns[aesthetics.yMin]
const yMaxCol = columns[aesthetics.yMax]
const fillCol = columns[aesthetics.fill]

const {hoverX, hoverY} = props.env

let hoveredRowIndices = null

if (hoverX && hoverY) {
hoveredRowIndices = findHoveredRowIndices(
xMinCol,
xMaxCol,
yMaxCol,
xScale.invert(hoverX),
yScale.invert(hoverY)
)
}

return (
<>
<HistogramBars
width={innerWidth}
height={innerHeight}
xMinCol={xMinCol}
xMaxCol={xMaxCol}
yMinCol={yMinCol}
yMaxCol={yMaxCol}
fillCol={fillCol}
fillScale={fillScale}
xScale={xScale}
yScale={yScale}
position={props.position || Position.Stacked}
hoveredRowIndices={hoveredRowIndices}
/>
{hoveredRowIndices && (
<HistogramTooltip
hoverX={hoverX}
hoverY={hoverY}
hoveredRowIndices={hoveredRowIndices}
width={innerWidth}
height={innerHeight}
xMinCol={xMinCol}
xMaxCol={xMaxCol}
yMinCol={yMinCol}
yMaxCol={yMaxCol}
fillCol={fillCol}
fillScale={fillScale}
x={x}
fill={fill}
tooltip={props.tooltip}
/>
)}
</>
)
}
Loading

0 comments on commit 93ccd1d

Please sign in to comment.