diff --git a/client/web/src/charts/components/bar-chart/BarChart.tsx b/client/web/src/charts/components/bar-chart/BarChart.tsx index add04a2eb72..e195476463c 100644 --- a/client/web/src/charts/components/bar-chart/BarChart.tsx +++ b/client/web/src/charts/components/bar-chart/BarChart.tsx @@ -1,18 +1,14 @@ -import { ReactElement, SVGProps, useMemo, useRef, useState } from 'react' +import { ReactElement, SVGProps, useMemo } from 'react' import { scaleBand, scaleLinear } from '@visx/scale' -import classNames from 'classnames' +import { ScaleBand } from 'd3-scale' import { noop } from 'lodash' -import { AxisBottom, AxisLeft, getChartContentSizes, Tooltip } from '../../core' +import { SvgAxisBottom, SvgAxisLeft, SvgContent, SvgRoot } from '../../core/components/SvgRoot' import { CategoricalLikeChart } from '../../types' -import { GroupedBars } from './components/GroupedBars' -import { StackedBars } from './components/StackedBars' -import { BarTooltipContent } from './components/TooltipContent' -import { Category, getGroupedCategories } from './utils/get-grouped-categories' - -import styles from './BarChart.module.scss' +import { BarChartContent } from './BarChartContent' +import { getGroupedCategories } from './utils/get-grouped-categories' const DEFAULT_LINK_GETTER = (): null => null @@ -23,18 +19,12 @@ interface BarChartProps extends CategoricalLikeChart, SVGProps string | undefined } -interface ActiveSegment { - category: Category - datum: Datum -} - export function BarChart(props: BarChartProps): ReactElement { const { width: outerWidth, height: outerHeight, data, stacked = false, - className, getDatumName, getDatumValue, getDatumColor, @@ -44,26 +34,6 @@ export function BarChart(props: BarChartProps): ReactElement { ...attributes } = props - const rootRef = useRef(null) - const [yAxisElement, setYAxisElement] = useState(null) - const [xAxisReference, setXAxisElement] = useState(null) - const [activeSegment, setActiveSegment] = useState | null>(null) - - const content = useMemo( - () => - getChartContentSizes({ - width: outerWidth, - height: outerHeight, - margin: { - top: 16, - right: 16, - left: yAxisElement?.getBBox().width, - bottom: xAxisReference?.getBBox().height, - }, - }), - [yAxisElement, xAxisReference, outerWidth, outerHeight] - ) - const categories = useMemo( () => getGroupedCategories({ data, stacked, getCategory, getDatumName, getDatumValue }), [data, stacked, getCategory, getDatumName, getDatumValue] @@ -73,19 +43,17 @@ export function BarChart(props: BarChartProps): ReactElement { () => scaleBand({ domain: categories.map(category => category.id), - range: [0, content.width], padding: 0.2, }), - [content, categories] + [categories] ) const yScale = useMemo( () => scaleLinear({ domain: [0, Math.max(...categories.map(category => category.maxValue))], - range: [content.height, 0], }), - [content, categories] + [categories] ) const handleBarClick = (datum: Datum): void => { @@ -98,78 +66,32 @@ export function BarChart(props: BarChartProps): ReactElement { onDatumLinkClick(datum) } - const withActiveLink = activeSegment?.datum ? getDatumLink(activeSegment?.datum) : null - return ( - - + + + - - - {stacked ? ( - setActiveSegment({ datum, category })} - onBarLeave={() => setActiveSegment(null)} - onBarClick={handleBarClick} - /> - ) : ( - setActiveSegment({ datum, category })} - onBarLeave={() => setActiveSegment(null)} - onBarClick={handleBarClick} - /> - )} - - {activeSegment && rootRef.current && ( - - , any>> + {({ yScale, xScale, content }) => ( + + // Visx axis interfaces doesn't support scaleLiner scale in + // axisScale interface + yScale={yScale} + xScale={xScale} + width={content.width} + height={content.height} + top={content.top} + left={content.left} + stacked={stacked} + categories={categories} getDatumName={getDatumName} + getDatumValue={getDatumValue} + getDatumColor={getDatumColor} + getDatumLink={getDatumLink} + onBarClick={handleBarClick} /> - - )} - + )} + + ) } diff --git a/client/web/src/charts/components/bar-chart/BarChart.module.scss b/client/web/src/charts/components/bar-chart/BarChartContent.module.scss similarity index 100% rename from client/web/src/charts/components/bar-chart/BarChart.module.scss rename to client/web/src/charts/components/bar-chart/BarChartContent.module.scss diff --git a/client/web/src/charts/components/bar-chart/BarChartContent.tsx b/client/web/src/charts/components/bar-chart/BarChartContent.tsx new file mode 100644 index 00000000000..5c8ed451ff9 --- /dev/null +++ b/client/web/src/charts/components/bar-chart/BarChartContent.tsx @@ -0,0 +1,110 @@ +import { ReactElement, SVGProps, useRef, useState } from 'react' + +import { Group } from '@visx/group' +import classNames from 'classnames' +import { ScaleBand, ScaleLinear } from 'd3-scale' + +import { Tooltip } from '../../core' + +import { GroupedBars } from './components/GroupedBars' +import { StackedBars } from './components/StackedBars' +import { BarTooltipContent } from './components/TooltipContent' +import { Category } from './utils/get-grouped-categories' + +import styles from './BarChartContent.module.scss' + +interface ActiveSegment { + category: Category + datum: Datum +} + +interface BarChartContentProps extends SVGProps { + stacked: boolean + + top: number + left: number + + xScale: ScaleBand + yScale: ScaleLinear + categories: Category[] + + getDatumName: (datum: Datum) => string + getDatumValue: (datum: Datum) => number + getDatumColor: (datum: Datum) => string | undefined + getDatumLink: (datum: Datum) => string | undefined | null + onBarClick: (datum: Datum) => void +} + +export function BarChartContent(props: BarChartContentProps): ReactElement { + const { + xScale, + yScale, + categories, + stacked, + top, + left, + width = 0, + height = 0, + getDatumName, + getDatumValue, + getDatumColor, + getDatumLink, + onBarClick, + ...attributes + } = props + + const rootRef = useRef(null) + const [activeSegment, setActiveSegment] = useState | null>(null) + + const withActiveLink = activeSegment?.datum ? getDatumLink(activeSegment?.datum) : null + + return ( + + {stacked ? ( + setActiveSegment({ datum, category })} + onBarLeave={() => setActiveSegment(null)} + onBarClick={onBarClick} + /> + ) : ( + setActiveSegment({ datum, category })} + onBarLeave={() => setActiveSegment(null)} + onBarClick={onBarClick} + /> + )} + + {activeSegment && rootRef.current && ( + + + + )} + + ) +} diff --git a/client/web/src/charts/components/line-chart/LineChart.tsx b/client/web/src/charts/components/line-chart/LineChart.tsx index 6a6a33097b0..97ffb2ce4c4 100644 --- a/client/web/src/charts/components/line-chart/LineChart.tsx +++ b/client/web/src/charts/components/line-chart/LineChart.tsx @@ -1,14 +1,15 @@ import { ReactElement, useMemo, useState, SVGProps, CSSProperties, useRef } from 'react' -import { AxisScale, TickFormatter } from '@visx/axis/lib/types' import { Group } from '@visx/group' -import { scaleTime, scaleLinear } from '@visx/scale' +import { scaleTime, scaleLinear, getTicks } from '@visx/scale' +import { AnyD3Scale } from '@visx/scale/lib/types/Scale' import { LinePath } from '@visx/shape' import { voronoi } from '@visx/voronoi' import classNames from 'classnames' import { noop } from 'lodash' import { AxisLeft, AxisBottom } from '../../core' +import { formatDateTick } from '../../core/components/axis/tick-formatters' import { SeriesLikeChart } from '../../types' import { Tooltip, TooltipContent, PointGlyph } from './components' @@ -24,7 +25,6 @@ import { getChartContentSizes, getMinMaxBoundaries, SeriesWithData, - formatXTick, } from './utils' import styles from './LineChart.module.scss' @@ -67,6 +67,21 @@ const sortByDataKey = (dataKey: string | number | symbol, activeDataKey: string) const identity = (argument: T): T => argument +interface GetScaleTicksInput { + scale: AnyD3Scale + space: number + pixelsPerTick?: number +} + +export function getXScaleTicks(input: GetScaleTicksInput): T[] { + const { scale, space, pixelsPerTick = 80 } = input + + // Calculate desirable number of ticks + const numberTicks = Math.max(2, Math.floor(space / pixelsPerTick)) + + return getTicks(scale, numberTicks) as T[] +} + /** * Visual component that renders svg line chart with pre-defined sizes, tooltip, * voronoi area distribution. @@ -211,7 +226,8 @@ export function LineChart(props: LineChartProps): ReactElement | null { width={content.width} top={content.bottom} left={content.left} - tickFormat={(formatXTick as unknown) as TickFormatter} + tickValues={getXScaleTicks({ scale: xScale, space: content.width })} + tickFormat={formatDateTick} /> diff --git a/client/web/src/charts/components/line-chart/components/tooltip/TooltipContent.tsx b/client/web/src/charts/components/line-chart/components/tooltip/TooltipContent.tsx index a0fdb3158b0..aa480f67841 100644 --- a/client/web/src/charts/components/line-chart/components/tooltip/TooltipContent.tsx +++ b/client/web/src/charts/components/line-chart/components/tooltip/TooltipContent.tsx @@ -4,8 +4,9 @@ import { isDefined } from '@sourcegraph/common' import { H3 } from '@sourcegraph/wildcard' import { TooltipList, TooltipListBlankItem, TooltipListItem } from '../../../../core' +import { formatDateTick } from '../../../../core/components/axis/tick-formatters' import { Point } from '../../types' -import { isValidNumber, formatYTick, SeriesWithData, SeriesDatum, getDatumValue, getLineColor } from '../../utils' +import { isValidNumber, SeriesWithData, SeriesDatum, getDatumValue, getLineColor } from '../../utils' import { getListWindow } from './utils/get-list-window' @@ -66,9 +67,10 @@ export function TooltipContent(props: TooltipContentProps): ReactE ... and {lines.leftRemaining} more )} {lines.window.map(line => { - const value = formatYTick(line.value) + const value = formatDateTick((line.value as unknown) as Date) const isActiveLine = activePoint.seriesId === line.id - const stackedValue = isActiveLine && stacked ? formatYTick(activePoint.value) : null + const stackedValue = + isActiveLine && stacked ? formatDateTick((activePoint.value as unknown) as Date) : null return ( {() => story()}], + parameters: { chromatic: { disableSnapshots: false } }, +} + +export default StoryConfig + +interface TemplateProps { + xScale: AxisScale + yScale: AxisScale + pixelsPerXTick?: number + formatXLabel?: (value: any) => string + color?: string +} + +const SimpleChartTemplate: Story = args => ( + + {parent => ( + + + + + + {({ content }) => } + + + )} + +) + +export const MainAxisDemo: Story = () => ( +
+ ({ + domain: [new Date(2022, 8, 22), new Date(2022, 10, 22)], + nice: true, + clamp: true, + })} + yScale={scaleLinear({ + domain: [0, boolean('useMaxValuesForYScale', false) ? 1000000000000000000000000000000000000 : 10000], + nice: true, + clamp: true, + })} + formatXLabel={formatDateTick} + pixelsPerXTick={20} + color="darkslateblue" + /> + + ({ + domain: ['hello', 'worlddddddddddd', 'this', 'is', 'rotation', 'speaking'], + padding: 0.2, + })} + yScale={scaleLinear({ + domain: [0, boolean('useMaxValuesForYScale', false) ? 1000000000000000000000000000000000000 : 10000], + nice: true, + clamp: true, + })} + color="pink" + /> +
+) diff --git a/client/web/src/charts/core/components/SvgRoot.tsx b/client/web/src/charts/core/components/SvgRoot.tsx new file mode 100644 index 00000000000..2d47fdb7c71 --- /dev/null +++ b/client/web/src/charts/core/components/SvgRoot.tsx @@ -0,0 +1,240 @@ +import { + createContext, + Dispatch, + FC, + PropsWithChildren, + ReactElement, + ReactNode, + SetStateAction, + SVGProps, + useContext, + useMemo, + useRef, + useState, +} from 'react' + +import { AxisScale, TickRendererProps } from '@visx/axis' +import { Group } from '@visx/group' +import { scaleLinear } from '@visx/scale' +import { noop } from 'lodash' +import { useMergeRefs } from 'use-callback-ref' +import useResizeObserver from 'use-resize-observer' + +import { createRectangle, EMPTY_RECTANGLE, Rectangle } from '@sourcegraph/wildcard' + +import { AxisBottom, AxisLeft } from './axis/Axis' +import { getMaxTickWidth, Tick, TickProps } from './axis/Tick' +import { getXScaleTicks } from './axis/tick-formatters' + +const DEFAULT_PADDING = { top: 16, right: 36, bottom: 0, left: 0 } + +interface Padding { + top: number + right: number + bottom: number + left: number +} + +interface SVGRootLayout { + width: number + height: number + yScale: AxisScale + xScale: AxisScale + content: Rectangle + setPadding: Dispatch> +} + +const SVGRootContext = createContext({ + width: 0, + height: 0, + xScale: scaleLinear(), + yScale: scaleLinear(), + content: EMPTY_RECTANGLE, + setPadding: noop, +}) + +interface SvgRootProps extends SVGProps { + width: number + height: number + yScale: AxisScale + xScale: AxisScale +} + +/** + * SVG canvas root element. This component renders SVG element and + * calculates and prepares all important canvas measurements for x/y-axis, + * content and other chart elements. + */ +export const SvgRoot: FC> = props => { + const { width, height, yScale: yOriginalScale, xScale: xOriginalScale, children, ...attributes } = props + + const [padding, setPadding] = useState(DEFAULT_PADDING) + + const contentRectangle = useMemo( + () => + createRectangle( + padding.left, + padding.top, + width - padding.left - padding.right, + height - padding.top - padding.bottom + ), + [width, height, padding] + ) + + const yScale = useMemo(() => yOriginalScale.copy().range([contentRectangle.height, 0]) as AxisScale, [ + yOriginalScale, + contentRectangle, + ]) + + const xScale = useMemo(() => xOriginalScale.copy().range([0, contentRectangle.width]) as AxisScale, [ + xOriginalScale, + contentRectangle, + ]) + + const context = useMemo( + () => ({ + width, + height, + xScale, + yScale, + content: contentRectangle, + setPadding, + }), + [width, height, contentRectangle, xScale, yScale] + ) + + return ( + + + {children} + + + ) +} + +interface SvgAxisLeftProps {} + +export const SvgAxisLeft: FC = props => { + const { content, yScale, setPadding } = useContext(SVGRootContext) + + const handleResize = ({ width = 0 }): void => { + setPadding(padding => ({ ...padding, left: width })) + } + + const { ref } = useResizeObserver({ onResize: handleResize }) + + return ( + + ) +} + +const defaultToString = (tick: T): string => `${tick}` +const defaultTruncatedTick = (tick: string): string => (tick.length >= 15 ? `${tick.slice(0, 15)}...` : tick) + +// TODO: Support reverse truncation for some charts https://github.com/sourcegraph/sourcegraph/issues/39879 +export const reverseTruncatedTick = (tick: string): string => (tick.length >= 15 ? `...${tick.slice(-15)}` : tick) + +interface SvgAxisBottomProps { + tickFormat?: (tick: Tick) => string + pixelsPerTick?: number + maxRotateAngle?: number + getTruncatedTick?: (formattedTick: string) => string +} + +export function SvgAxisBottom(props: SvgAxisBottomProps): ReactElement { + const { + pixelsPerTick = 0, + maxRotateAngle = 90, + tickFormat = defaultToString, + getTruncatedTick = defaultTruncatedTick, + } = props + const { content, xScale, setPadding } = useContext(SVGRootContext) + + const axisGroupRef = useRef(null) + const { ref } = useResizeObserver({ + // TODO: Fix corner cases with axis sizes see https://github.com/sourcegraph/sourcegraph/issues/39876 + onResize: ({ height = 0 }) => setPadding(padding => ({ ...padding, bottom: height })), + }) + + const [, upperRangeBound] = xScale.range() as [number, number] + const ticks = getXScaleTicks({ scale: xScale, space: content.width, pixelsPerTick }) + + const maxWidth = useMemo(() => { + const axisGroup = axisGroupRef.current + + if (!axisGroup) { + return 0 + } + + return getMaxTickWidth(axisGroup, ticks.map(tickFormat)) + }, [ticks, tickFormat]) + + const getXTickProps = (props: TickRendererProps): TickProps => { + const measuredSize = ticks.length * maxWidth + const rotate = + upperRangeBound < measuredSize + ? maxRotateAngle * Math.min(1, (measuredSize / upperRangeBound - 0.8) / 2) + : 0 + + if (rotate) { + return { + ...props, + // Truncate ticks only if we rotate them, this means truncate labels only + // when they overlap + getTruncatedTick, + transform: `rotate(${rotate}, ${props.x} ${props.y})`, + textAnchor: 'start', + } + } + + return { ...props, textAnchor: 'middle' } + } + + return ( + } + tickFormat={tickFormat} + /> + ) +} + +interface SvgContentProps { + children: (input: { xScale: XScale; yScale: YScale; content: Rectangle }) => ReactNode +} + +/** + * Compound svg canvas component, to render actual chart content on + * SVG canvas with pre-calculated axes and paddings + */ +export function SvgContent( + props: SvgContentProps +): ReactElement { + const { children } = props + const { content, xScale, yScale } = useContext(SVGRootContext) + + return ( + + {children({ + // We need to cast scales here because there is no other way to type context + // shared data in TS, React interfaces. + xScale: xScale as XScale, + yScale: yScale as YScale, + content, + })} + + ) +} diff --git a/client/web/src/charts/core/components/axis/Axis.module.scss b/client/web/src/charts/core/components/axis/Axis.module.scss index be9a25ed914..5b5c47dec8d 100644 --- a/client/web/src/charts/core/components/axis/Axis.module.scss +++ b/client/web/src/charts/core/components/axis/Axis.module.scss @@ -27,13 +27,6 @@ stroke-width: 1; } - // tick label - text { - fill: var(--text-muted); - font-size: 0.75rem; - font-weight: 400; - } - &--vertical { line { // Hide line ticks visually and hide them from voice over diff --git a/client/web/src/charts/core/components/axis/Axis.tsx b/client/web/src/charts/core/components/axis/Axis.tsx index 279771c1add..e887b8365e7 100644 --- a/client/web/src/charts/core/components/axis/Axis.tsx +++ b/client/web/src/charts/core/components/axis/Axis.tsx @@ -1,29 +1,49 @@ import { forwardRef, memo } from 'react' -import { AxisLeft as VisxAxisLeft, AxisBottom as VisxAsixBottom } from '@visx/axis' -import { AxisScale, TickFormatter } from '@visx/axis/lib/types' +import { + AxisLeft as VisxAxisLeft, + AxisBottom as VisxAsixBottom, + TickLabelProps, + SharedAxisProps, + AxisScale, +} from '@visx/axis' import { GridRows } from '@visx/grid' import { Group } from '@visx/group' +import { TextProps } from '@visx/text' import classNames from 'classnames' -import { formatYTick, getXScaleTicks, getYScaleTicks } from '../../../components/line-chart/utils' - -import { getTickXProps, getTickYProps, Tick } from './Tick' +import { Tick } from './Tick' +import { formatYTick, getXScaleTicks, getYScaleTicks } from './tick-formatters' import styles from './Axis.module.scss' -interface AxisLeftProps { - top: number - left: number +// TODO: Remove this prop generation, see https://github.com/sourcegraph/sourcegraph/issues/39874 +const getTickYLabelProps: TickLabelProps = (value, index, values): Partial => ({ + dy: '0.25em', + textAnchor: 'end', + 'aria-label': `Tick axis ${index + 1} of ${values.length}. Value: ${value}`, +}) + +type OwnSharedAxisProps = Omit, 'tickLabelProps'> + +export interface AxisLeftProps extends OwnSharedAxisProps { width: number height: number - scale: AxisScale } export const AxisLeft = memo( forwardRef((props, reference) => { - const { scale, left, top, width, height } = props - const ticksValues = getYScaleTicks({ scale, space: height }) + const { + scale, + left, + top, + width, + height, + tickComponent = Tick, + tickFormat = formatYTick, + tickValues = getYScaleTicks({ scale, space: height }), + ...attributes + } = props return ( <> @@ -33,22 +53,18 @@ export const AxisLeft = memo( width={width} height={height} scale={scale} - tickValues={ticksValues} + tickValues={tickValues} className={styles.gridLine} /> - `${store}-${tick}`, '')} - innerRef={reference} - top={top} - left={left} - > + @@ -58,26 +74,30 @@ export const AxisLeft = memo( }) ) -interface AxisBottomProps { - top: number - left: number +AxisLeft.displayName = 'AxisLeft' + +// TODO: Remove this prop generation, see https://github.com/sourcegraph/sourcegraph/issues/39874 +const getTickXLabelProps: TickLabelProps = (value, index, values): Partial => ({ + 'aria-label': `Tick axis ${index + 1} of ${values.length}. Value: ${value}`, + textAnchor: 'middle', +}) + +interface AxisBottomProps extends OwnSharedAxisProps { width: number - scale: AxisScale - tickFormat?: TickFormatter } export const AxisBottom = memo( forwardRef((props, reference) => { - const { scale, top, left, width, tickFormat } = props + const { scale, top, left, width, tickValues, tickComponent = Tick, ...attributes } = props return ( - + @@ -85,3 +105,5 @@ export const AxisBottom = memo( ) }) ) + +AxisBottom.displayName = 'AxisBottom' diff --git a/client/web/src/charts/core/components/axis/Tick.module.scss b/client/web/src/charts/core/components/axis/Tick.module.scss new file mode 100644 index 00000000000..ddb029484d6 --- /dev/null +++ b/client/web/src/charts/core/components/axis/Tick.module.scss @@ -0,0 +1,4 @@ +.tick { + fill: var(--text-muted); + font-size: 0.75rem; +} diff --git a/client/web/src/charts/core/components/axis/Tick.tsx b/client/web/src/charts/core/components/axis/Tick.tsx index 8affa66274b..4d3aab85f70 100644 --- a/client/web/src/charts/core/components/axis/Tick.tsx +++ b/client/web/src/charts/core/components/axis/Tick.tsx @@ -1,35 +1,19 @@ -import React from 'react' +import { FC } from 'react' -import { TickLabelProps, TickRendererProps } from '@visx/axis' +import { TickRendererProps } from '@visx/axis' import { Group } from '@visx/group' import { Text, TextProps } from '@visx/text' +import classNames from 'classnames' -import { formatXLabel } from '../../../components/line-chart/utils' +import styles from './Tick.module.scss' -export const getTickYProps: TickLabelProps = (value, index, values): Partial => ({ - dx: '-0.25em', - dy: '0.25em', - fill: '#222', - fontFamily: 'Arial', - fontSize: 10, - textAnchor: 'end', - 'aria-label': `Tick axis ${index + 1} of ${values.length}. Value: ${value}`, -}) +export interface TickProps extends TickRendererProps { + getTruncatedTick?: (lable: string) => string +} -export const getTickXProps: TickLabelProps = (value, index, values): Partial => ({ - dy: '0.25em', - fill: '#222', - fontFamily: 'Arial', - fontSize: 10, - textAnchor: 'middle', - 'aria-label': `Tick axis ${index + 1} of ${values.length}. Value: ${formatXLabel(value)}`, -}) - -/** - * Tick component displays tick label for each axis line of chart. - */ -export const Tick: React.FunctionComponent> = props => { - const { formattedValue, ...tickLabelProps } = props +/** Tick component displays tick label for each axis line of chart. */ +export const Tick: FC = props => { + const { formattedValue = '', 'aria-label': ariaLabel, className, getTruncatedTick, ...tickLabelProps } = props // Hack with Group + Text (aria hidden) // Because the Text component renders text inside svg element and text element with tspan @@ -38,10 +22,37 @@ export const Tick: React.FunctionComponent - - {formattedValue} + + + {getTruncatedTick ? getTruncatedTick(formattedValue) : formattedValue} ) } + +/** + * Text (labels) ticks measure helper. Since there is no way to measure text + * before rendering inside React tree we have to conduct pre-rendering measurements + * for ticks labels. + * + * It renders each labels (text tick) inside selection element with SVG text element + * and measures its sizes. + */ +export const getMaxTickWidth = (selection: Element, labels: string[]): number => { + const tester = document.createElementNS('http://www.w3.org/2000/svg', 'text') + + // In order to sync Tick component and pre-rendering text styles which is vital for + // text measurements + tester.classList.add(styles.tick) + selection.append(tester) + + const boundingBoxes = labels.map(label => { + tester.textContent = label + + return tester.getBBox() + }) + + tester.remove() + + return Math.max(...boundingBoxes.map(b => b.width)) +} diff --git a/client/web/src/charts/components/line-chart/utils/ticks.ts b/client/web/src/charts/core/components/axis/tick-formatters.ts similarity index 84% rename from client/web/src/charts/components/line-chart/utils/ticks.ts rename to client/web/src/charts/core/components/axis/tick-formatters.ts index 1cd451cf2df..63a6bb1949c 100644 --- a/client/web/src/charts/components/line-chart/utils/ticks.ts +++ b/client/web/src/charts/core/components/axis/tick-formatters.ts @@ -20,7 +20,7 @@ export function formatYTick(number: number): string { * * Example: 01 Jan, 12 Feb, ... */ -export const formatXTick = timeFormat('%d %b') +export const formatDateTick = timeFormat('%d %b') /** * Returns a formatted date text for points aria labels. @@ -37,11 +37,19 @@ interface GetScaleTicksInput { pixelsPerTick?: number } -export function getXScaleTicks(input: GetScaleTicksInput): number[] { +export function getXScaleTicks(input: GetScaleTicksInput): T[] { const { scale, space, pixelsPerTick = 80 } = input - const maxTicks = Math.max(Math.floor(space / pixelsPerTick), MINIMUM_NUMBER_OF_TICKS) - return getTicks(scale, maxTicks) as number[] + // Calculate desirable number of ticks + const numberTicks = Math.max(MINIMUM_NUMBER_OF_TICKS, Math.floor(space / pixelsPerTick)) + + let filteredTicks = getTicks(scale) + + while (filteredTicks.length > numberTicks) { + filteredTicks = getHalvedTicks(filteredTicks) + } + + return filteredTicks } /** @@ -83,7 +91,7 @@ export function getYScaleTicks(input: GetScaleTicksInput): number[] { * removes all even index ticks with even number removes all * odd index ticks. */ -function getHalvedTicks(ticks: number[]): number[] { +function getHalvedTicks(ticks: T[]): T[] { const isOriginTickLengthOdd = !(ticks.length % 2) const filteredTicks = [] diff --git a/client/web/src/charts/core/components/tooltip/Tooltip.tsx b/client/web/src/charts/core/components/tooltip/Tooltip.tsx index 6d73dee817e..47e518c3fb9 100644 --- a/client/web/src/charts/core/components/tooltip/Tooltip.tsx +++ b/client/web/src/charts/core/components/tooltip/Tooltip.tsx @@ -9,7 +9,7 @@ import styles from './Tooltip.module.scss' const TOOLTIP_PADDING = createRectangle(0, 0, 10, 10) interface TooltipProps { - containerElement: SVGSVGElement + containerElement: Element activeElement?: HTMLElement } @@ -35,6 +35,10 @@ export const Tooltip: React.FunctionComponent { + // We need this casting because Element type doesn't support + // pointer or mouse events in pointermove handlers + const element = containerElement as HTMLElement + function handleMove(event: PointerEvent): void { setVirtualElement({ target: null, @@ -43,10 +47,10 @@ export const Tooltip: React.FunctionComponent { - containerElement.removeEventListener('pointermove', handleMove) + element.removeEventListener('pointermove', handleMove) } }, [containerElement]) diff --git a/client/web/src/enterprise/insights/pages/insights/creation/capture-group/CaptureGroupCreationPage.story.tsx b/client/web/src/enterprise/insights/pages/insights/creation/capture-group/CaptureGroupCreationPage.story.tsx index 3421490d5ab..181fd2291a6 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/capture-group/CaptureGroupCreationPage.story.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/capture-group/CaptureGroupCreationPage.story.tsx @@ -9,7 +9,7 @@ import { CodeInsightsBackendContext, SeriesChartContent, CodeInsightsGqlBackend import { CaptureGroupCreationPage as CaptureGroupCreationPageComponent } from './CaptureGroupCreationPage' export default { - title: 'web/insights/creation-ui/CaptureGroupCreationPage', + title: 'web/insights/creation-ui/capture-group/CaptureGroupCreationPage', decorators: [story => {() =>
{story()}
}], parameters: { chromatic: { diff --git a/client/web/src/enterprise/insights/pages/insights/creation/compute/ComputeInsightCreationPage.story.tsx b/client/web/src/enterprise/insights/pages/insights/creation/compute/ComputeInsightCreationPage.story.tsx index 093f79f228b..a64de54f387 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/compute/ComputeInsightCreationPage.story.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/compute/ComputeInsightCreationPage.story.tsx @@ -11,7 +11,7 @@ import { SERIES_MOCK_CHART } from '../../../../components' import { ComputeInsightCreationPage as ComputeInsightCreationPageComponent } from './ComputeInsightCreationPage' const defaultStory: Meta = { - title: 'web/insights/creation-ui/ComputeInsightCreationPage', + title: 'web/insights/creation-ui/compute/ComputeInsightCreationPage', decorators: [story => {() => story()}], parameters: { chromatic: { diff --git a/client/web/src/enterprise/insights/pages/insights/creation/compute/components/ComputeInsightCreationContent.tsx b/client/web/src/enterprise/insights/pages/insights/creation/compute/components/ComputeInsightCreationContent.tsx index 2aa5a88fcb4..217fbec8464 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/compute/components/ComputeInsightCreationContent.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/compute/components/ComputeInsightCreationContent.tsx @@ -22,10 +22,10 @@ import { useForm, } from '../../../../../components' import { useUiFeatures } from '../../../../../hooks' -import { ComputeLivePreview } from '../../ComputeLivePreview' import { CreateComputeInsightFormFields } from '../types' import { ComputeInsightMapPicker } from './ComputeInsightMapPicker' +import { ComputeLivePreview } from './ComputeLivePreview' const INITIAL_INSIGHT_VALUES: CreateComputeInsightFormFields = { series: [createDefaultEditSeries({ edit: true })], diff --git a/client/web/src/enterprise/insights/pages/insights/creation/ComputeLivePreview.story.tsx b/client/web/src/enterprise/insights/pages/insights/creation/compute/components/ComputeLivePreview.story.tsx similarity index 89% rename from client/web/src/enterprise/insights/pages/insights/creation/ComputeLivePreview.story.tsx rename to client/web/src/enterprise/insights/pages/insights/creation/compute/components/ComputeLivePreview.story.tsx index 66c6e49e32b..4eb18d2a6ab 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/ComputeLivePreview.story.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/compute/components/ComputeLivePreview.story.tsx @@ -2,14 +2,14 @@ import { Meta, Story } from '@storybook/react' import { GroupByField } from '@sourcegraph/shared/src/graphql-operations' -import { WebStory } from '../../../../../components/WebStory' -import { CodeInsightsBackendStoryMock } from '../../../CodeInsightsBackendStoryMock' -import { BackendInsightDatum, SeriesChartContent } from '../../../core' +import { WebStory } from '../../../../../../../components/WebStory' +import { CodeInsightsBackendStoryMock } from '../../../../../CodeInsightsBackendStoryMock' +import { BackendInsightDatum, SeriesChartContent } from '../../../../../core' import { ComputeLivePreview as ComputeLivePreviewComponent } from './ComputeLivePreview' const defaultStory: Meta = { - title: 'web/insights/creation-ui/ComputeLivePreview', + title: 'web/insights/creation-ui/compute/ComputeLivePreview', decorators: [story => {() => story()}], } diff --git a/client/web/src/enterprise/insights/pages/insights/creation/ComputeLivePreview.tsx b/client/web/src/enterprise/insights/pages/insights/creation/compute/components/ComputeLivePreview.tsx similarity index 95% rename from client/web/src/enterprise/insights/pages/insights/creation/ComputeLivePreview.tsx rename to client/web/src/enterprise/insights/pages/insights/creation/compute/components/ComputeLivePreview.tsx index b1742496562..c6dd6d331cb 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/ComputeLivePreview.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/compute/components/ComputeLivePreview.tsx @@ -5,9 +5,9 @@ import { groupBy } from 'lodash' import { ErrorAlert } from '@sourcegraph/branded/src/components/alerts' import { useDeepMemo } from '@sourcegraph/wildcard' -import { LegendItem, LegendList, Series } from '../../../../../charts' -import { BarChart } from '../../../../../charts/components/bar-chart/BarChart' -import { GroupByField } from '../../../../../graphql-operations' +import { LegendItem, LegendList, Series } from '../../../../../../../charts' +import { BarChart } from '../../../../../../../charts/components/bar-chart/BarChart' +import { GroupByField } from '../../../../../../../graphql-operations' import { LivePreviewUpdateButton, LivePreviewCard, @@ -20,13 +20,13 @@ import { StateStatus, COMPUTE_MOCK_CHART, EditableDataSeries, -} from '../../../components' +} from '../../../../../components' import { BackendInsightDatum, CategoricalChartContent, CodeInsightsBackendContext, SeriesPreviewSettings, -} from '../../../core' +} from '../../../../../core' interface LanguageUsageDatum { name: string diff --git a/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/LangStatsInsightCreationPage.story.tsx b/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/LangStatsInsightCreationPage.story.tsx index 07ff25c2c15..b70f88c3301 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/LangStatsInsightCreationPage.story.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/LangStatsInsightCreationPage.story.tsx @@ -11,7 +11,7 @@ import { getRandomLangStatsMock } from './components/live-preview-chart/constant import { LangStatsInsightCreationPage as LangStatsInsightCreationPageComponent } from './LangStatsInsightCreationPage' const defaultStory: Meta = { - title: 'web/insights/creation-ui/LangStatsInsightCreationPage', + title: 'web/insights/creation-ui/lang-stats/LangStatsInsightCreationPage', decorators: [story => {() => story()}], parameters: { chromatic: { diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/SearchInsightCreationPage.story.tsx b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/SearchInsightCreationPage.story.tsx index 573ee511f5c..f84c4aee80e 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/SearchInsightCreationPage.story.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/SearchInsightCreationPage.story.tsx @@ -11,7 +11,7 @@ import { SERIES_MOCK_CHART } from '../../../../components' import { SearchInsightCreationPage as SearchInsightCreationPageComponent } from './SearchInsightCreationPage' const defaultStory: Meta = { - title: 'web/insights/creation-ui/SearchInsightCreationPage', + title: 'web/insights/creation-ui/search/SearchInsightCreationPage', decorators: [story => {() => story()}], parameters: { chromatic: { diff --git a/package.json b/package.json index a0aa970b3d2..5d5e88cda42 100644 --- a/package.json +++ b/package.json @@ -396,7 +396,7 @@ "@stripe/react-stripe-js": "^1.8.0-0", "@stripe/stripe-js": "^1.29.0", "@visx/annotation": "^2.10.0", - "@visx/axis": "^2.10.0", + "@visx/axis": "^2.11.1", "@visx/glyph": "^2.10.0", "@visx/grid": "^2.10.0", "@visx/group": "^2.10.0", @@ -486,7 +486,7 @@ "use-callback-ref": "^1.2.5", "use-debounce": "^8.0.1", "use-deep-compare-effect": "^1.6.1", - "use-resize-observer": "^7.0.0", + "use-resize-observer": "^9.0.2", "utility-types": "^3.10.0", "uuid": "^8.3.0", "webext-domain-permission-toggle": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index a659c81f5f1..bd384ff8f0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6710,16 +6710,16 @@ prop-types "^15.5.10" react-use-measure "^2.0.4" -"@visx/axis@^2.10.0": - version "2.10.0" - resolved "https://registry.npmjs.org/@visx/axis/-/axis-2.10.0.tgz#613fde76653edbefb795cafe2e11fc4c2294db46" - integrity sha512-myEcXPzD7ZmKiXuhue2lpiuTDgl3Glhe1LB+xoUDS8ZAW76Asd6PwurjoxSnq3tHCz0EDBh7YlgApeFy3Bw38A== +"@visx/axis@^2.11.1": + version "2.11.1" + resolved "https://registry.npmjs.org/@visx/axis/-/axis-2.11.1.tgz#e1cc978ede9cce197cd7712183301e9e89c519e2" + integrity sha512-RZdT+yhAEOXtcLc3PgD14Xh5bJh5B64IG+zvHe3YVMkiFWGT1phy0sGTRRxritHke16ErB9vndx+pwVIGSkxAw== dependencies: "@types/react" "*" "@visx/group" "2.10.0" "@visx/point" "2.6.0" "@visx/scale" "2.2.2" - "@visx/shape" "2.10.0" + "@visx/shape" "2.11.1" "@visx/text" "2.10.0" classnames "^2.3.1" prop-types "^15.6.0" @@ -6831,6 +6831,24 @@ lodash "^4.17.21" prop-types "^15.5.10" +"@visx/shape@2.11.1": + version "2.11.1" + resolved "https://registry.npmjs.org/@visx/shape/-/shape-2.11.1.tgz#ff41d839ad689fea53504597bd4bfbc8b7c20bbb" + integrity sha512-0ak3wTkXjExH7kzU62yXPu+RtuG35G1sNL58Ax4NBr4yKh2nTHcLRsMZ7k6yUG+wSjb8DgG/ywZ0bBGWXJYGbg== + dependencies: + "@types/d3-path" "^1.0.8" + "@types/d3-shape" "^1.3.1" + "@types/lodash" "^4.14.172" + "@types/react" "*" + "@visx/curve" "2.1.0" + "@visx/group" "2.10.0" + "@visx/scale" "2.2.2" + classnames "^2.3.1" + d3-path "^1.0.5" + d3-shape "^1.2.0" + lodash "^4.17.21" + prop-types "^15.5.10" + "@visx/text@2.10.0": version "2.10.0" resolved "https://registry.npmjs.org/@visx/text/-/text-2.10.0.tgz#2ff413f4dd35617e45b24d6082d7b856358afe4c" @@ -24952,12 +24970,12 @@ use-deep-compare-effect@^1.6.1: "@types/react" "^17.0.0" dequal "^2.0.2" -use-resize-observer@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-7.0.0.tgz#15f0efbd5a4e08a8cc51901f21a89ba836f2116e" - integrity sha512-+RjrQsk/mL8aKy4TGBDiPkUv6whyeoGDMIZYk0gOGHOlnrsjImC+jG6lfAFcBCKAG9epGRL419adhDNdkDCQkA== +use-resize-observer@^9.0.2: + version "9.0.2" + resolved "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.0.2.tgz#25830221933d9b6e931850023305eb9d24379a6b" + integrity sha512-JOzsmF3/IDmtjG7OE5qXOP69LEpBpwhpLSiT1XgSr+uFRX0ftJHQnDaP7Xq+uhbljLYkJt67sqsbnyXBjiY8ig== dependencies: - resize-observer-polyfill "^1.5.1" + "@juggle/resize-observer" "^3.3.1" use-sidecar@^1.0.1, use-sidecar@^1.0.5: version "1.0.5"