Skip to content

Commit

Permalink
feat(FOROME-804): implemented logarithmic scale for Y-axis in range s…
Browse files Browse the repository at this point in the history
…lider histogram (#528)

* feat(FOROME-739): implemented histograms for numeric attributes in sidebar

* feat(FOROME-739): implemented histogram, bar chart and pie chart with d3.js

* feat(FOROME-804): implemented logarithmic scale for Y-axis in range slider histogram
  • Loading branch information
AlMaQntr authored Apr 3, 2022
1 parent 3e8ec81 commit 08d6a71
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const StrictnessSelect = ({
<div className={cn('relative', className)}>
<div
className={cn(
'flex items-center w-12 p-1 shadow-dark rounded',
'flex items-center w-12 p-1 shadow-dark rounded bg-white',
isDisabled
? 'text-grey-blue'
: 'cursor-pointer text-black hover:text-blue-bright',
Expand Down
61 changes: 61 additions & 0 deletions src/core/charts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as d3 from 'd3'

import { formatNumber } from '@core/format-number'

export const getBounds = <T>(
data: T[],
valueGetter: (item: T) => number,
): [number, number] => {
let min = valueGetter(data[0])
let max = min

for (const item of data) {
const value = valueGetter(item)
min = Math.min(min, value)
max = Math.max(max, value)
}

return [min, max]
}

type TGetYScaleAndAxisParams = {
min: number
max: number
height: number
minTickDistance?: number
}

export const getYScaleAndAxis = ({
min,
max,
height,
minTickDistance = 36,
}: TGetYScaleAndAxisParams): [
d3.ScaleContinuousNumeric<number, number>,
d3.Axis<number>,
] => {
const logMin = Math.max(Math.floor(Math.log10(min)), 0)
const logMax = Math.ceil(Math.log10(max))

const isLogScale = logMax - logMin >= 3
const scale = isLogScale ? d3.scaleLog() : d3.scaleLinear()
scale.range([height, 0])

if (isLogScale) {
scale.domain([Math.pow(10, logMin), max])
} else {
scale.domain([0, max])
}

const ticksCount = Math.min(
Math.floor(height / minTickDistance) + 1,
isLogScale ? logMax - logMin - 1 : 4,
)

const axis = d3
.axisLeft<number>(scale)
.ticks(ticksCount)
.tickFormat(formatNumber)

return [scale, axis]
}
4 changes: 3 additions & 1 deletion src/core/format-number.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const intlNumberFormat = new Intl.NumberFormat().format
export const intlNumberFormat = new Intl.NumberFormat(undefined, {
maximumFractionDigits: 16,
}).format

export const formatNumber = (value: unknown): string => {
return typeof value === 'number' ? intlNumberFormat(value) : '...'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as d3 from 'd3'

import { getBounds, getYScaleAndAxis } from '@core/charts'
import { formatNumber } from '@core/format-number'
import { t } from '@i18n'
import { SvgChartRenderParams } from '@components/svg-chart/svg-chart'
import { TVariant } from '@service-providers/common'
import { TBarChartData } from '../chart.interface'
import { getBounds, getYScaleAndAxis } from '../utils'
import { barColor, tickColor } from './bar-chart.styles'

const margin = {
Expand Down Expand Up @@ -46,7 +46,7 @@ export const drawBarChart = ({
.padding(0.1)
.align(0)

const [yScale, yAxis] = getYScaleAndAxis(min, max, chartHeight)
const [yScale, yAxis] = getYScaleAndAxis({ min, max, height: chartHeight })

chart
.append('g')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as d3 from 'd3'

import { getBounds, getYScaleAndAxis } from '@core/charts'
import { formatNumber } from '@core/format-number'
import { t } from '@i18n'
import { SvgChartRenderParams } from '@components/svg-chart'
Expand All @@ -9,7 +10,6 @@ import {
THistogramChartData,
THistogramChartDataItem,
} from '../chart.interface'
import { getBounds, getYScaleAndAxis } from '../utils'
import { barColor, logBarColor } from './histogram-chart.styles'

const margin = {
Expand Down Expand Up @@ -86,7 +86,7 @@ export const drawHistogram = ({

const noZeroData = data.filter(({ value }) => value > 0)
const [min, max] = getBounds(noZeroData, item => item.value)
const [yScale, yAxis] = getYScaleAndAxis(min, max, chartHeight)
const [yScale, yAxis] = getYScaleAndAxis({ min, max, height: chartHeight })

const isLogMode = mode === HistogramTypes.LOG
const xScale = isLogMode ? d3.scaleLog() : d3.scaleLinear()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './reduceVariantsData'
export * from './scaling'
export * from './useChartConfig'
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { ReactElement, useEffect, useMemo, useRef } from 'react'

import { formatNumber } from '@core/format-number'
import { RangeSliderColor } from '../types'
import {
RangeSliderHistogramAxisLabel,
Expand Down Expand Up @@ -27,10 +28,7 @@ export const RangeSliderHistogram = ({
}: IRangeSliderHistogramProps): ReactElement | null => {
const canvasRef = useRef<HTMLCanvasElement>(null)

const yAxis = useMemo(
() => getYAxis(Math.max(...data), height),
[data, height],
)
const yAxis = useMemo(() => getYAxis(data, height), [data, height])

const barPositions = useMemo(
() =>
Expand Down Expand Up @@ -80,14 +78,14 @@ export const RangeSliderHistogram = ({
height: `${height}px`,
}}
/>
{yAxis.map(([value, offset]) => (
{yAxis.ticks.map(value => (
<RangeSliderHistogramAxisLabel
key={value}
style={{
top: offset,
top: `${yAxis.scale(value)}px`,
}}
>
{value}
{formatNumber(value)}
</RangeSliderHistogramAxisLabel>
))}
</RangeSliderHistogramRoot>
Expand Down
43 changes: 23 additions & 20 deletions src/ui/range-slider/range-slider-histogram/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ScaleContinuousNumeric } from 'd3'

import { getBounds, getYScaleAndAxis } from '@core/charts'
import { Color, color2str, interpolateColor, parseColor } from '@core/colors'
import { theme } from '@theme'
import { RangeSliderColor } from '../types'
Expand All @@ -23,18 +26,19 @@ export const prepareCanvas = (
canvas.height = height * pixelRatio
}

type HistogramAxis = [number, number][]

export const getYAxis = (max: number, height: number): HistogramAxis => {
const k = Math.pow(10, Math.floor(Math.log10(max)))
const step = Math.ceil(max / 4 / k) * k
type HistogramAxis = {
scale: ScaleContinuousNumeric<number, number>
ticks: number[]
}

const ret: [number, number][] = []
for (let y = step; y <= max; y += step) {
ret.push([y, (1 - y / max) * height])
}
export const getYAxis = (data: number[], height: number): HistogramAxis => {
const [min, max] = getBounds(
data.filter(value => value > 0),
item => item,
)
const [scale, axis] = getYScaleAndAxis({ min, max, height })

return ret
return { scale, ticks: scale.ticks(...axis.tickArguments()) }
}

const getBarColor = (color: RangeSliderColor): Color =>
Expand Down Expand Up @@ -119,7 +123,7 @@ type DrawHistogramParams = {
height: number
data: number[]
selectedArea?: [number, number] | null
yAxis?: HistogramAxis
yAxis: HistogramAxis
barPositions: number[]
barSpacing?: number
partialFill?: HistogramPartialFill
Expand All @@ -141,17 +145,14 @@ export const drawHistogram = ({
const drawWidth = width * pixelRatio
const drawHeight = height * pixelRatio

const max = Math.max(...data)
const yScale = drawHeight / max

ctx.clearRect(0, 0, drawWidth, drawHeight)

if (yAxis) {
ctx.beginPath()
ctx.strokeStyle = axisCssColor

for (const point of yAxis) {
const y = point[1] * pixelRatio
for (const value of yAxis.ticks) {
const y = yAxis.scale(value) * pixelRatio

ctx.moveTo(0, y)
ctx.lineTo(drawWidth, y)
Expand Down Expand Up @@ -185,6 +186,8 @@ export const drawHistogram = ({
const x0 = Math.max(barPositions[i], 0) * pixelRatio
const x1 = Math.min(barPositions[i + 1] ?? width, width) * pixelRatio
const barWidth = x1 - x0
const y0 = yAxis.scale(data[i]) * pixelRatio
const y1 = drawHeight

if (!selectedArea || x0 >= selectedRight || x1 <= selectedLeft) {
ctx.fillStyle = inactiveBarCssColor
Expand All @@ -201,8 +204,8 @@ export const drawHistogram = ({
right,
x0,
x1,
y0: drawHeight - data[i] * yScale,
y1: drawHeight,
y0,
y1,
color,
}

Expand All @@ -224,9 +227,9 @@ export const drawHistogram = ({

ctx.fillRect(
x0 + (i > 0 ? halfBarDrawSpacing : 0),
drawHeight - value * yScale,
y0,
barWidth - (i < barCount - 1 ? halfBarDrawSpacing : 0),
value * yScale,
y1 - y0,
)
}
}
7 changes: 5 additions & 2 deletions src/ui/range-slider/range-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, {
} from 'react'
import cn, { Argument } from 'classnames'

import { formatNumber } from '@core/format-number'
import { RangeSliderHistogram } from './range-slider-histogram'
import {
RangeSliderHandle,
Expand Down Expand Up @@ -308,11 +309,13 @@ export const RangeSlider = ({
/>
))}
{leftValue !== null && (
<RangeSliderLabel ref={leftLabelRef}>{leftValue}</RangeSliderLabel>
<RangeSliderLabel ref={leftLabelRef}>
{formatNumber(leftValue)}
</RangeSliderLabel>
)}
{isRangeMode && rightValue !== null && (
<RangeSliderLabel ref={rightLabelRef}>
{rightValue}
{formatNumber(rightValue)}
</RangeSliderLabel>
)}
{isRangeMode && (
Expand Down
2 changes: 1 addition & 1 deletion src/ui/range-slider/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface IRangeSliderRootProps {

export const RangeSliderRoot = styled.div<IRangeSliderRootProps>`
${props => props.isVertical && 'height: 100%;'}
margin-left: ${props => (props.hasHistogram ? '40px' : '0')};
margin-left: ${props => (props.hasHistogram ? '48px' : '0')};
cursor: ${props =>
props.isDisabled ? 'default' : props.isActive ? 'grabbing' : 'pointer'};
`
Expand Down

0 comments on commit 08d6a71

Please sign in to comment.