From eac308773b3b23991b5b7c15604622ec7810e79e Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Sat, 23 Sep 2023 11:44:53 +0300 Subject: [PATCH 1/9] feat(D3 plugin): axis labels rotation --- src/plugins/d3/examples/bar-x/Basic.tsx | 1 + src/plugins/d3/renderer/components/AxisX.tsx | 8 +- src/plugins/d3/renderer/components/Chart.tsx | 7 +- src/plugins/d3/renderer/components/Legend.tsx | 12 ++- .../d3/renderer/constants/defaults/axis.ts | 14 +++ .../d3/renderer/constants/defaults/legend.ts | 2 +- src/plugins/d3/renderer/constants/index.ts | 1 - .../d3/renderer/hooks/useAxisScales/index.ts | 14 ++- .../hooks/useChartDimensions/index.ts | 71 +++----------- .../renderer/hooks/useChartOptions/chart.ts | 15 +-- .../renderer/hooks/useChartOptions/index.ts | 9 +- .../renderer/hooks/useChartOptions/types.ts | 11 ++- .../renderer/hooks/useChartOptions/x-axis.ts | 96 +++++++++++++++++-- .../renderer/hooks/useChartOptions/y-axis.ts | 8 +- .../hooks/useSeries/prepare-legend.ts | 3 +- .../renderer/utils/axis-generators/bottom.ts | 33 ++----- src/plugins/d3/renderer/utils/axis.ts | 25 ++++- src/plugins/d3/renderer/utils/text.ts | 40 +++++--- src/types/widget-data/axis.ts | 10 +- 19 files changed, 226 insertions(+), 154 deletions(-) diff --git a/src/plugins/d3/examples/bar-x/Basic.tsx b/src/plugins/d3/examples/bar-x/Basic.tsx index 789b0ae3..65c4d973 100644 --- a/src/plugins/d3/examples/bar-x/Basic.tsx +++ b/src/plugins/d3/examples/bar-x/Basic.tsx @@ -40,6 +40,7 @@ export const BasicBarXChart = () => { text: 'Game Platforms', }, }, + yAxis: [{title: {text: 'Number of games released'}}], }; return ; diff --git a/src/plugins/d3/renderer/components/AxisX.tsx b/src/plugins/d3/renderer/components/AxisX.tsx index 4c3f0e7c..809f0429 100644 --- a/src/plugins/d3/renderer/components/AxisX.tsx +++ b/src/plugins/d3/renderer/components/AxisX.tsx @@ -59,7 +59,7 @@ export const AxisX = React.memo(({axis, width, height, scale}: Props) => { labelsStyle: axis.labels.style, count: getTicksCount({axis, range: width}), maxTickCount: getMaxTickCount({axis, width}), - autoRotation: axis.labels.autoRotation, + rotation: axis.labels.rotation, }, domain: { size: width, @@ -77,15 +77,15 @@ export const AxisX = React.memo(({axis, width, height, scale}: Props) => { // add an axis header if necessary if (axis.title.text) { - const textY = - axis.title.height + parseInt(axis.labels.style.fontSize) + axis.labels.padding; + const y = + axis.title.height + axis.title.margin + axis.labels.height + axis.labels.margin; svgElement .append('text') .attr('class', b('title')) .attr('text-anchor', 'middle') .attr('x', width / 2) - .attr('y', textY) + .attr('y', y) .attr('font-size', axis.title.style.fontSize) .text(axis.title.text) .call(setEllipsisForOverflowText, width); diff --git a/src/plugins/d3/renderer/components/Chart.tsx b/src/plugins/d3/renderer/components/Chart.tsx index f438cb97..261d5286 100644 --- a/src/plugins/d3/renderer/components/Chart.tsx +++ b/src/plugins/d3/renderer/components/Chart.tsx @@ -17,6 +17,7 @@ import {AxisX} from './AxisX'; import {Legend} from './Legend'; import {Title} from './Title'; import {Tooltip, TooltipTriggerArea} from './Tooltip'; +import {getPreparedXAxis} from '../hooks/useChartOptions/x-axis'; import './styles.scss'; @@ -37,9 +38,13 @@ export const Chart = (props: Props) => { const dispatcher = React.useMemo(() => { return getD3Dispatcher(); }, []); - const {chart, title, tooltip, xAxis, yAxis} = useChartOptions({ + const {chart, title, tooltip, yAxis} = useChartOptions({ data, }); + const xAxis = React.useMemo( + () => getPreparedXAxis({xAxis: data.xAxis, width, series: data.series.data}), + [data, width], + ); const { legendItems, legendConfig, diff --git a/src/plugins/d3/renderer/components/Legend.tsx b/src/plugins/d3/renderer/components/Legend.tsx index 2f0e36ed..adac06cb 100644 --- a/src/plugins/d3/renderer/components/Legend.tsx +++ b/src/plugins/d3/renderer/components/Legend.tsx @@ -123,12 +123,14 @@ export const Legend = (props: Props) => { .enter() .append('g') .attr('class', b('item')) + .attr('transform', 'translate(0, )') .on('click', function (e, d) { onItemClick({name: d.name, metaKey: e.metaKey}); }) .each(function (d) { textWidths.push(d.textWidth); }); + legendItemTemplate .append('rect') .attr('x', function (legendItem, i) { @@ -140,8 +142,7 @@ export const Legend = (props: Props) => { ); }) .attr('y', (legendItem) => { - const lineOffset = legend.lineHeight * lineIndex; - return config.offset.top + lineOffset - legendItem.symbol.height / 2; + return Math.max(0, (legend.lineHeight - legendItem.symbol.height) / 2); }) .attr('width', (legendItem) => { return legendItem.symbol.width; @@ -166,7 +167,7 @@ export const Legend = (props: Props) => { textWidths.slice(0, i).reduce((acc, tw) => acc + tw, 0) ); }) - .attr('y', config.offset.top + legend.lineHeight * lineIndex) + .attr('height', legend.lineHeight) .attr('class', function (d) { const mods = {selected: d.visible, unselected: !d.visible}; return b('item-text', mods); @@ -175,7 +176,7 @@ export const Legend = (props: Props) => { return ('name' in d && d.name) as string; }) .style('font-size', legend.itemStyle.fontSize) - .style('alignment-baseline', 'middle'); + .style('alignment-baseline', 'before-edge'); const contentWidth = legendLine.node()?.getBoundingClientRect().width || 0; const {left} = getLegendPosition({ @@ -184,8 +185,9 @@ export const Legend = (props: Props) => { offsetWidth: config.offset.left, contentWidth, }); + const top = config.offset.top + legend.lineHeight * lineIndex; - legendLine.attr('transform', `translate(${[left, 0].join(',')})`); + legendLine.attr('transform', `translate(${[left, top].join(',')})`); }); if (config.pagination) { diff --git a/src/plugins/d3/renderer/constants/defaults/axis.ts b/src/plugins/d3/renderer/constants/defaults/axis.ts index 11fe72a0..94ecfc29 100644 --- a/src/plugins/d3/renderer/constants/defaults/axis.ts +++ b/src/plugins/d3/renderer/constants/defaults/axis.ts @@ -3,3 +3,17 @@ export const axisLabelsDefaults = { padding: 10, fontSize: 11, }; + +const axisTitleDefaults = { + fontSize: '14px', +}; + +export const xAxisTitleDefaults = { + ...axisTitleDefaults, + margin: 0, +}; + +export const yAxisTitleDefaults = { + ...axisTitleDefaults, + margin: 10, +}; diff --git a/src/plugins/d3/renderer/constants/defaults/legend.ts b/src/plugins/d3/renderer/constants/defaults/legend.ts index 680b660e..359b43a0 100644 --- a/src/plugins/d3/renderer/constants/defaults/legend.ts +++ b/src/plugins/d3/renderer/constants/defaults/legend.ts @@ -6,7 +6,7 @@ type LegendDefaults = Required> & export const legendDefaults: LegendDefaults = { align: 'center', itemDistance: 20, - margin: 15, + margin: 12, itemStyle: { fontSize: '12px', }, diff --git a/src/plugins/d3/renderer/constants/index.ts b/src/plugins/d3/renderer/constants/index.ts index 945a2ce3..1c995d20 100644 --- a/src/plugins/d3/renderer/constants/index.ts +++ b/src/plugins/d3/renderer/constants/index.ts @@ -24,4 +24,3 @@ export const DEFAULT_PALETTE = [ ]; export const DEFAULT_AXIS_LABEL_FONT_SIZE = '11px'; -export const DEFAULT_AXIS_TITLE_FONT_SIZE = '14px'; diff --git a/src/plugins/d3/renderer/hooks/useAxisScales/index.ts b/src/plugins/d3/renderer/hooks/useAxisScales/index.ts index 727861db..25cbefd7 100644 --- a/src/plugins/d3/renderer/hooks/useAxisScales/index.ts +++ b/src/plugins/d3/renderer/hooks/useAxisScales/index.ts @@ -14,6 +14,7 @@ import { } from '../../utils'; import type {AxisDirection} from '../../utils'; import {PreparedSeries} from '../useSeries/types'; +import {ChartKitWidgetAxis, ChartKitWidgetSeries} from '../../../../../types'; export type ChartScale = | ScaleLinear @@ -24,7 +25,7 @@ type Args = { boundsWidth: number; boundsHeight: number; series: PreparedSeries[]; - xAxis: ChartOptions['xAxis']; + xAxis: PreparedAxis; yAxis: ChartOptions['yAxis']; }; @@ -40,7 +41,7 @@ const isNumericalArrayData = (data: unknown[]): data is number[] => { const filterCategoriesByVisibleSeries = (args: { axisDirection: AxisDirection; categories: string[]; - series: PreparedSeries[]; + series: (PreparedSeries | ChartKitWidgetSeries)[]; }) => { const {axisDirection, categories, series} = args; @@ -109,13 +110,18 @@ export function createYScale(axis: PreparedAxis, series: PreparedSeries[], bound throw new Error('Failed to create yScale'); } -export function createXScale(axis: PreparedAxis, series: PreparedSeries[], boundsWidth: number) { +export function createXScale( + axis: PreparedAxis | ChartKitWidgetAxis, + series: (PreparedSeries | ChartKitWidgetSeries)[], + boundsWidth: number, +) { const xMin = get(axis, 'min'); const xType = get(axis, 'type', 'linear'); const xCategories = get(axis, 'categories'); const xTimestamps = get(axis, 'timestamps'); + const maxPadding = get(axis, 'maxPadding', 0); - const xAxisMinPadding = boundsWidth * axis.maxPadding; + const xAxisMinPadding = boundsWidth * maxPadding; const xRange = [0, boundsWidth - xAxisMinPadding]; switch (xType) { diff --git a/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts b/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts index e04001ca..48585ce1 100644 --- a/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts +++ b/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts @@ -1,20 +1,8 @@ -import {AxisDomain, AxisScale} from 'd3'; import React from 'react'; import type {ChartMargin} from '../../../../../types'; import type {PreparedAxis, PreparedLegend, PreparedSeries} from '../../hooks'; -import {createXScale} from '../../hooks'; -import { - formatAxisTickLabel, - getClosestPointsRange, - getHorisontalSvgTextHeight, - getLabelsMaxHeight, - getMaxTickCount, - getTicksCount, - getXAxisItems, - hasOverlappingLabels, - isAxisRelatedSeries, -} from '../../utils'; +import {isAxisRelatedSeries} from '../../utils'; import {getBoundsWidth} from './utils'; export {getBoundsWidth} from './utils'; @@ -29,48 +17,15 @@ type Args = { preparedSeries: PreparedSeries[]; }; -const getHeightOccupiedByXAxis = ({ - preparedXAxis, - preparedSeries, - width, -}: { - preparedXAxis: PreparedAxis; - preparedSeries: PreparedSeries[]; - width: number; -}) => { - let height = preparedXAxis.title.height; +const getHeightOccupiedByXAxis = ({preparedXAxis}: {preparedXAxis: PreparedAxis}) => { + let height = 0; - if (preparedXAxis.labels.enabled) { - const scale = createXScale(preparedXAxis, preparedSeries, width); - const tickCount = getTicksCount({axis: preparedXAxis, range: width}); - const ticks = getXAxisItems({ - scale: scale as AxisScale, - count: tickCount, - maxCount: getMaxTickCount({width, axis: preparedXAxis}), - }); - const step = getClosestPointsRange(preparedXAxis, ticks); - const labels = ticks.map((value: AxisDomain) => { - return formatAxisTickLabel({ - axis: preparedXAxis, - value, - step, - }); - }); - const overlapping = hasOverlappingLabels({ - width, - labels, - padding: preparedXAxis.labels.padding, - style: preparedXAxis.labels.style, - }); + if (preparedXAxis.title) { + height += preparedXAxis.title.height + preparedXAxis.title.margin; + } - const labelsHeight = overlapping - ? getLabelsMaxHeight({ - labels, - style: preparedXAxis.labels.style, - transform: 'rotate(-45)', - }) - : getHorisontalSvgTextHeight({text: 'Tmp', style: preparedXAxis.labels.style}); - height += preparedXAxis.labels.margin + labelsHeight; + if (preparedXAxis.labels.enabled) { + height += preparedXAxis.labels.margin + preparedXAxis.labels.height; } return height; @@ -80,10 +35,8 @@ const getBottomOffset = (args: { hasAxisRelatedSeries: boolean; preparedLegend: PreparedLegend; preparedXAxis: PreparedAxis; - preparedSeries: PreparedSeries[]; - width: number; }) => { - const {hasAxisRelatedSeries, preparedLegend, preparedXAxis, preparedSeries, width} = args; + const {hasAxisRelatedSeries, preparedLegend, preparedXAxis} = args; let result = 0; if (preparedLegend.enabled) { @@ -91,9 +44,11 @@ const getBottomOffset = (args: { } if (hasAxisRelatedSeries) { - result += getHeightOccupiedByXAxis({preparedXAxis, preparedSeries, width}); + result += getHeightOccupiedByXAxis({preparedXAxis}); } + console.log({result, preparedLegend, xAxis: getHeightOccupiedByXAxis({preparedXAxis})}); + return result; }; @@ -108,8 +63,6 @@ export const useChartDimensions = (args: Args) => { hasAxisRelatedSeries, preparedLegend, preparedXAxis, - preparedSeries, - width: boundsWidth, }); const boundsHeight = height - margin.top - margin.bottom - bottomOffset; diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts b/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts index 9ad778d8..734f4461 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts @@ -1,26 +1,19 @@ import get from 'lodash/get'; -import type {ChartKitWidgetData} from '../../../../../types/widget-data'; +import type {ChartKitWidgetData} from '../../../../../types'; -import {isAxisRelatedSeries, getHorisontalSvgTextHeight} from '../../utils'; +import {isAxisRelatedSeries} from '../../utils'; import type {PreparedAxis, PreparedChart, PreparedTitle} from './types'; const AXIS_LINE_WIDTH = 1; const getMarginTop = (args: { chart: ChartKitWidgetData['chart']; - hasAxisRelatedSeries: boolean; - preparedY1Axis: PreparedAxis; preparedTitle?: PreparedTitle; }) => { - const {chart, hasAxisRelatedSeries, preparedY1Axis, preparedTitle} = args; + const {chart, preparedTitle} = args; let marginTop = get(chart, 'margin.top', 0); - if (hasAxisRelatedSeries) { - marginTop += - getHorisontalSvgTextHeight({text: 'Tmp', style: preparedY1Axis.labels.style}) / 2; - } - if (preparedTitle?.height) { marginTop += preparedTitle.height; } @@ -61,7 +54,7 @@ export const getPreparedChart = (args: { }): PreparedChart => { const {chart, series, preparedY1Axis, preparedTitle} = args; const hasAxisRelatedSeries = series.data.some(isAxisRelatedSeries); - const marginTop = getMarginTop({chart, hasAxisRelatedSeries, preparedY1Axis, preparedTitle}); + const marginTop = getMarginTop({chart, preparedTitle}); const marginBottom = get(chart, 'margin.bottom', 0); const marginLeft = getMarginLeft({chart, hasAxisRelatedSeries, preparedY1Axis}); const marginRight = getMarginRight({chart}); diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/index.ts b/src/plugins/d3/renderer/hooks/useChartOptions/index.ts index 3c0c098d..506d5716 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/index.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/index.ts @@ -1,11 +1,10 @@ import React from 'react'; -import type {ChartKitWidgetData} from '../../../../../types/widget-data'; +import type {ChartKitWidgetData} from '../../../../../types'; import {getPreparedChart} from './chart'; import {getPreparedTitle} from './title'; import {getPreparedTooltip} from './tooltip'; -import {getPreparedXAxis} from './x-axis'; import {getPreparedYAxis} from './y-axis'; import type {ChartOptions} from './types'; @@ -15,13 +14,12 @@ type Args = { export const useChartOptions = (args: Args): ChartOptions => { const { - data: {chart, series, title, tooltip, xAxis, yAxis}, + data: {chart, series, title, tooltip, yAxis}, } = args; const options: ChartOptions = React.useMemo(() => { const preparedTitle = getPreparedTitle({title}); const preparedTooltip = getPreparedTooltip({tooltip}); const preparedYAxis = getPreparedYAxis({series: series.data, yAxis}); - const preparedXAxis = getPreparedXAxis({xAxis}); const preparedChart = getPreparedChart({ chart, series, @@ -32,10 +30,9 @@ export const useChartOptions = (args: Args): ChartOptions => { chart: preparedChart, title: preparedTitle, tooltip: preparedTooltip, - xAxis: preparedXAxis, yAxis: preparedYAxis, }; - }, [chart, title, tooltip, series, xAxis, yAxis]); + }, [chart, title, tooltip, series, yAxis]); return options; }; diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts index 911c4c06..429692b4 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts @@ -7,10 +7,15 @@ import type { ChartMargin, } from '../../../../../types/widget-data'; -type PreparedAxisLabels = Omit & - Required> & { +type PreparedAxisLabels = Omit< + ChartKitWidgetAxisLabels, + 'enabled' | 'padding' | 'style' | 'autoRotation' +> & + Required> & { style: BaseTextStyle; maxWidth?: number; + rotation: number; + height: number; }; export type PreparedChart = { @@ -23,6 +28,7 @@ export type PreparedAxis = Omit & { title: { height: number; text: string; + margin: number; style: BaseTextStyle; }; min?: number; @@ -46,7 +52,6 @@ export type PreparedTooltip = ChartKitWidgetData['tooltip'] & { export type ChartOptions = { chart: PreparedChart; tooltip: PreparedTooltip; - xAxis: PreparedAxis; yAxis: PreparedAxis[]; title?: PreparedTitle; }; diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts b/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts index 179d1bb3..0201d36a 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts @@ -1,20 +1,86 @@ import get from 'lodash/get'; - -import type {ChartKitWidgetData} from '../../../../../types/widget-data'; - +import type {AxisDomain, AxisScale} from 'd3'; +import type {BaseTextStyle, ChartKitWidgetSeries, ChartKitWidgetAxis} from '../../../../../types'; import { axisLabelsDefaults, DEFAULT_AXIS_LABEL_FONT_SIZE, - DEFAULT_AXIS_TITLE_FONT_SIZE, + xAxisTitleDefaults, } from '../../constants'; import type {PreparedAxis} from './types'; -import {BaseTextStyle} from '../../../../../types/widget-data'; -import {getHorisontalSvgTextHeight} from '../../utils'; +import { + formatAxisTickLabel, + getClosestPointsRange, + getHorisontalSvgTextHeight, + getLabelsMaxHeight, + getMaxTickCount, + getTicksCount, + getXAxisItems, + hasOverlappingLabels, +} from '../../utils'; +import {createXScale} from '../useAxisScales'; + +function getLabelSettings({ + axis, + series, + width, + autoRotation = true, +}: { + axis: PreparedAxis; + series: ChartKitWidgetSeries[]; + width: number; + autoRotation?: boolean; +}) { + const scale = createXScale(axis, series, width); + const tickCount = getTicksCount({axis, range: width}); + const ticks = getXAxisItems({ + scale: scale as AxisScale, + count: tickCount, + maxCount: getMaxTickCount({width, axis}), + }); + const step = getClosestPointsRange(axis, ticks); + const labels = ticks.map((value: AxisDomain) => { + return formatAxisTickLabel({ + axis, + value, + step, + }); + }); + const overlapping = hasOverlappingLabels({ + width, + labels, + padding: axis.labels.padding, + style: axis.labels.style, + }); + + const defaultRotation = overlapping && autoRotation ? -45 : 0; + const rotation = axis.labels.rotation || defaultRotation; -export const getPreparedXAxis = ({xAxis}: {xAxis: ChartKitWidgetData['xAxis']}): PreparedAxis => { + const labelsHeight = rotation + ? getLabelsMaxHeight({ + labels, + style: { + 'font-size': axis.labels.style.fontSize, + 'font-weight': axis.labels.style.fontWeight || 'normal', + }, + rotation, + }) + : getHorisontalSvgTextHeight({text: 'Tmp', style: axis.labels.style}); + + return {height: labelsHeight, rotation}; +} + +export const getPreparedXAxis = ({ + xAxis, + series, + width, +}: { + xAxis?: ChartKitWidgetAxis; + series: ChartKitWidgetSeries[]; + width: number; +}): PreparedAxis => { const titleText = get(xAxis, 'title.text', ''); const titleStyle: BaseTextStyle = { - fontSize: get(xAxis, 'title.style.fontSize', DEFAULT_AXIS_TITLE_FONT_SIZE), + fontSize: get(xAxis, 'title.style.fontSize', xAxisTitleDefaults.fontSize), }; const preparedXAxis: PreparedAxis = { @@ -25,8 +91,9 @@ export const getPreparedXAxis = ({xAxis}: {xAxis: ChartKitWidgetData['xAxis']}): padding: get(xAxis, 'labels.padding', axisLabelsDefaults.padding), dateFormat: get(xAxis, 'labels.dateFormat'), numberFormat: get(xAxis, 'labels.numberFormat'), - autoRotation: get(xAxis, 'labels.autoRotation', true), + rotation: get(xAxis, 'labels.rotation', 0), style: {fontSize: get(xAxis, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE)}, + height: 0, }, lineColor: get(xAxis, 'lineColor'), categories: get(xAxis, 'categories'), @@ -34,6 +101,7 @@ export const getPreparedXAxis = ({xAxis}: {xAxis: ChartKitWidgetData['xAxis']}): title: { text: titleText, style: titleStyle, + margin: get(xAxis, 'title.margin', xAxisTitleDefaults.margin), height: titleText ? getHorisontalSvgTextHeight({text: titleText, style: titleStyle}) : 0, @@ -48,5 +116,15 @@ export const getPreparedXAxis = ({xAxis}: {xAxis: ChartKitWidgetData['xAxis']}): }, }; + const {height, rotation} = getLabelSettings({ + axis: preparedXAxis, + series, + width, + autoRotation: xAxis?.labels?.autoRotation, + }); + + preparedXAxis.labels.height = height; + preparedXAxis.labels.rotation = rotation; + return preparedXAxis; }; diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts b/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts index 4a0fe149..1108a5e9 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts @@ -10,7 +10,7 @@ import type { import { axisLabelsDefaults, DEFAULT_AXIS_LABEL_FONT_SIZE, - DEFAULT_AXIS_TITLE_FONT_SIZE, + yAxisTitleDefaults, } from '../../constants'; import { getHorisontalSvgTextHeight, @@ -74,7 +74,7 @@ export const getPreparedYAxis = ({ }; const y1TitleText = get(yAxis1, 'title.text', ''); const y1TitleStyle: BaseTextStyle = { - fontSize: get(yAxis1, 'title.style.fontSize', DEFAULT_AXIS_TITLE_FONT_SIZE), + fontSize: get(yAxis1, 'title.style.fontSize', yAxisTitleDefaults.fontSize), }; const preparedY1Axis: PreparedAxis = { type: get(yAxis1, 'type', 'linear'), @@ -82,16 +82,18 @@ export const getPreparedYAxis = ({ enabled: get(yAxis1, 'labels.enabled', true), margin: get(yAxis1, 'labels.margin', axisLabelsDefaults.margin), padding: get(yAxis1, 'labels.padding', axisLabelsDefaults.padding), - autoRotation: get(yAxis1, 'labels.autoRotation', false), dateFormat: get(yAxis1, 'labels.dateFormat'), numberFormat: get(yAxis1, 'labels.numberFormat'), style: y1LabelsStyle, + rotation: 0, + height: 0, }, lineColor: get(yAxis1, 'lineColor'), categories: get(yAxis1, 'categories'), timestamps: get(yAxis1, 'timestamps'), title: { text: y1TitleText, + margin: get(yAxis1, 'title.margin', yAxisTitleDefaults.margin), style: y1TitleStyle, height: y1TitleText ? getHorisontalSvgTextHeight({text: y1TitleText, style: y1TitleStyle}) diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts index 8549fcd8..c9208c2a 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts @@ -123,8 +123,7 @@ export const getLegendComponents = (args: { } preparedLegend.height = legendHeight; - const top = - chartHeight - chartMargin.bottom - preparedLegend.height + preparedLegend.lineHeight / 2; + const top = chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.height; const offset: LegendConfig['offset'] = {left: chartMargin.left, top}; return {legendConfig: {offset, pagination}, legendItems: items}; diff --git a/src/plugins/d3/renderer/utils/axis-generators/bottom.ts b/src/plugins/d3/renderer/utils/axis-generators/bottom.ts index 7ddc8ea1..b2eec67c 100644 --- a/src/plugins/d3/renderer/utils/axis-generators/bottom.ts +++ b/src/plugins/d3/renderer/utils/axis-generators/bottom.ts @@ -1,8 +1,8 @@ import type {AxisDomain, AxisScale, Selection} from 'd3'; import {select} from 'd3'; import {BaseTextStyle} from '../../../../../types'; -import {getXAxisItems, getXAxisOffset, getXTickPosition} from '../axis'; -import {hasOverlappingLabels, setEllipsisForOverflowText} from '../text'; +import {getXAxisItems, getXAxisOffset, getXTickPosition, rotateLabels} from '../axis'; +import {setEllipsisForOverflowText} from '../text'; type AxisBottomArgs = { scale: AxisScale; @@ -14,7 +14,7 @@ type AxisBottomArgs = { labelsMargin?: number; labelsStyle?: BaseTextStyle; size: number; - autoRotation?: boolean; + rotation: number; }; domain: { size: number; @@ -52,7 +52,7 @@ export function axisBottom(args: AxisBottomArgs) { size: tickSize, count: ticksCount, maxTickCount, - autoRotation = true, + rotation, }, domain: {size: domainSize, color: domainColor}, } = args; @@ -93,27 +93,10 @@ export function axisBottom(args: AxisBottomArgs) { .select('line') .remove(); - const labels = selection.selectAll('.tick text'); - const labelNodes = labels.nodes() as SVGTextElement[]; - - const overlapping = hasOverlappingLabels({ - width: domainSize, - labels: values.map(labelFormat), - padding: labelsPaddings, - style: labelsStyle, - }); - - const rotationAngle = overlapping && autoRotation ? '-45' : undefined; - - if (rotationAngle) { - const labelHeight = labelNodes[0]?.getBoundingClientRect()?.height; - const labelOffset = (labelHeight / 2 + labelsMargin) / 2; - labels - .attr('text-anchor', 'end') - .attr( - 'transform', - `rotate(${rotationAngle}) translate(-${labelOffset}, -${labelOffset})`, - ); + const labels = selection.selectAll('.tick text'); + + if (rotation) { + rotateLabels(labels, {rotation, margin: labelsMargin}); } else { // remove overlapping labels let elementX = 0; diff --git a/src/plugins/d3/renderer/utils/axis.ts b/src/plugins/d3/renderer/utils/axis.ts index 1a0927bc..9f61b4d9 100644 --- a/src/plugins/d3/renderer/utils/axis.ts +++ b/src/plugins/d3/renderer/utils/axis.ts @@ -1,5 +1,5 @@ import {PreparedAxis} from '../hooks'; -import {AxisDomain, AxisScale, ScaleBand} from 'd3'; +import {AxisDomain, AxisScale, ScaleBand, Selection} from 'd3'; export function getTicksCount({axis, range}: {axis: PreparedAxis; range: number}) { let ticksCount: number | undefined; @@ -64,3 +64,26 @@ export function getMaxTickCount({axis, width}: {axis: PreparedAxis; width: numbe const minTickWidth = parseInt(axis.labels.style.fontSize) + axis.labels.padding; return Math.floor(width / minTickWidth); } + +export function rotateLabels( + selection: Selection, + { + rotation, + margin, + }: { + rotation: number; + margin: number; + }, +) { + selection + .attr('text-anchor', rotation > 0 ? 'start' : 'end') + .style('transform-box', 'fill-box') + .style('transform', `rotate(${rotation}deg)`); + + if (rotation < 0) { + selection.style('transform-origin', function () { + const labelWidth = (this as Element)?.getBoundingClientRect()?.width || 0; + return `${labelWidth}px ${margin}px`; + }); + } +} diff --git a/src/plugins/d3/renderer/utils/text.ts b/src/plugins/d3/renderer/utils/text.ts index 5b1bf1d4..02433a11 100644 --- a/src/plugins/d3/renderer/utils/text.ts +++ b/src/plugins/d3/renderer/utils/text.ts @@ -1,6 +1,7 @@ import type {Selection} from 'd3'; import {select} from 'd3'; import {BaseTextStyle} from '../../../../types'; +import {rotateLabels} from './axis'; export function setEllipsisForOverflowText( selection: Selection, @@ -58,19 +59,24 @@ function renderLabels( selection: Selection, { labels, - style, - transform, + style = {}, + attrs = {}, }: { labels: string[]; - style?: BaseTextStyle; - transform?: string; + style?: Record; + attrs?: Record; }, ) { - const text = selection - .append('g') - .append('text') - .attr('transform', transform || '') - .style('font-size', style?.fontSize || ''); + const text = selection.append('g').append('text'); + + Object.entries(style).forEach(([name, value]) => { + text.style(name, value); + }); + + Object.entries(attrs).forEach(([name, value]) => { + text.attr(name, value); + }); + text.selectAll('tspan') .data(labels) .enter() @@ -88,11 +94,12 @@ export function getLabelsMaxWidth({ transform, }: { labels: string[]; - style?: BaseTextStyle; + style?: Record; transform?: string; }) { const svg = select(document.body).append('svg'); - svg.call(renderLabels, {labels, style, transform}); + const attrs: Record = transform ? {transform: transform} : {}; + svg.call(renderLabels, {labels, style, attrs}); const maxWidth = (svg.select('g').node() as Element)?.getBoundingClientRect()?.width || 0; svg.remove(); @@ -103,14 +110,17 @@ export function getLabelsMaxWidth({ export function getLabelsMaxHeight({ labels, style, - transform, + rotation, }: { labels: string[]; - style?: BaseTextStyle; - transform?: string; + style?: Record; + rotation?: number; }) { const svg = select(document.body).append('svg'); - svg.call(renderLabels, {labels, style, transform}); + const textSelection = renderLabels(svg, {labels, style}); + if (rotation) { + rotateLabels(textSelection, {rotation, margin: 0}); + } const maxHeight = (svg.select('g').node() as Element)?.getBoundingClientRect()?.height || 0; svg.remove(); diff --git a/src/types/widget-data/axis.ts b/src/types/widget-data/axis.ts index f7af55f6..7a5a3ac9 100644 --- a/src/types/widget-data/axis.ts +++ b/src/types/widget-data/axis.ts @@ -6,27 +6,24 @@ export type ChartKitWidgetAxisType = 'category' | 'datetime' | 'linear'; export type ChartKitWidgetAxisLabels = { /** Enable or disable the axis labels. */ enabled?: boolean; - /** The label's pixel distance from the perimeter of the plot area. * * @default: 10 */ margin?: number; - /** The pixel padding for axis labels, to ensure white space between them. * * @defaults: 5 * */ padding?: number; - dateFormat?: string; numberFormat?: FormatNumberOptions; style?: Partial; - /** For horizontal axes, enable label rotation to prevent overlapping labels. * If there is enough space, labels are not rotated. * As the chart gets narrower, it will start rotating the labels -45 degrees. */ autoRotation?: boolean; + rotation?: number; }; export type ChartKitWidgetAxis = { @@ -39,6 +36,11 @@ export type ChartKitWidgetAxis = { lineColor?: string; title?: { text?: string; + /** The pixel distance between the axis labels or line and the title. + * + * Defaults to 4 for horizontal axes, 10 for vertical. + * */ + margin?: number; }; /** The minimum value of the axis. If undefined the min value is automatically calculate. */ min?: number; From f926147f529e69c8f27a54801fd1b272429aedad Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Mon, 25 Sep 2023 18:59:26 +0300 Subject: [PATCH 2/9] fix legend --- src/plugins/d3/renderer/components/AxisY.tsx | 2 +- src/plugins/d3/renderer/components/Chart.tsx | 6 ++- src/plugins/d3/renderer/components/Legend.tsx | 41 +++++++++-------- .../d3/renderer/constants/defaults/legend.ts | 2 +- .../hooks/useChartDimensions/index.ts | 24 +++------- .../hooks/useChartDimensions/utils.ts | 28 +++++++++--- .../renderer/hooks/useChartOptions/chart.ts | 27 +----------- .../renderer/hooks/useChartOptions/types.ts | 2 +- .../renderer/hooks/useChartOptions/x-axis.ts | 1 + .../renderer/hooks/useChartOptions/y-axis.ts | 8 +++- .../hooks/useSeries/prepare-legend.ts | 11 +++-- .../renderer/utils/axis-generators/bottom.ts | 44 +++++++++++++++---- src/plugins/d3/renderer/utils/axis.ts | 25 +---------- src/plugins/d3/renderer/utils/text.ts | 5 ++- 14 files changed, 115 insertions(+), 111 deletions(-) diff --git a/src/plugins/d3/renderer/components/AxisY.tsx b/src/plugins/d3/renderer/components/AxisY.tsx index e3345fd9..4899d133 100644 --- a/src/plugins/d3/renderer/components/AxisY.tsx +++ b/src/plugins/d3/renderer/components/AxisY.tsx @@ -100,7 +100,7 @@ export const AxisY = ({axises, width, height, scale}: Props) => { .remove(); if (axis.title.text) { - const textY = axis.title.height + axis.labels.margin; + const textY = axis.title.margin + axis.labels.margin + axis.labels.width; svgElement .append('text') diff --git a/src/plugins/d3/renderer/components/Chart.tsx b/src/plugins/d3/renderer/components/Chart.tsx index 261d5286..f541def6 100644 --- a/src/plugins/d3/renderer/components/Chart.tsx +++ b/src/plugins/d3/renderer/components/Chart.tsx @@ -20,6 +20,7 @@ import {Tooltip, TooltipTriggerArea} from './Tooltip'; import {getPreparedXAxis} from '../hooks/useChartOptions/x-axis'; import './styles.scss'; +import {getWidthOccupiedByYAxis} from '../hooks/useChartDimensions/utils'; const b = block('d3'); @@ -92,6 +93,9 @@ export const Chart = (props: Props) => { svgContainer: svgRef.current, }); + const boundsOffsetTop = chart.margin.top; + const boundsOffsetLeft = chart.margin.left + getWidthOccupiedByYAxis({preparedAxis: yAxis}); + return ( @@ -99,7 +103,7 @@ export const Chart = (props: Props) => { {xScale && yScale && ( diff --git a/src/plugins/d3/renderer/components/Legend.tsx b/src/plugins/d3/renderer/components/Legend.tsx index adac06cb..5d9b0e85 100644 --- a/src/plugins/d3/renderer/components/Legend.tsx +++ b/src/plugins/d3/renderer/components/Legend.tsx @@ -53,6 +53,7 @@ const appendPaginator = (args: { const {container, offset, maxPage, legend, transform, onArrowClick} = args; const paginationLine = container.append('g').attr('class', b('pagination')); let computedWidth = 0; + paginationLine .append('text') .text('▲') @@ -115,7 +116,6 @@ export const Legend = (props: Props) => { ? items.slice(paginationOffset * limit, paginationOffset * limit + limit) : items; pageItems.forEach((line, lineIndex) => { - const textWidths: number[] = []; const legendLine = svgElement.append('g').attr('class', b('line')); const legendItemTemplate = legendLine .selectAll('legend-history') @@ -123,23 +123,26 @@ export const Legend = (props: Props) => { .enter() .append('g') .attr('class', b('item')) - .attr('transform', 'translate(0, )') .on('click', function (e, d) { onItemClick({name: d.name, metaKey: e.metaKey}); - }) - .each(function (d) { - textWidths.push(d.textWidth); }); - legendItemTemplate - .append('rect') - .attr('x', function (legendItem, i) { + const getXPosition = (i: number) => { + return line.slice(0, i).reduce((acc, legendItem) => { return ( - i * legendItem.symbol.width + - i * legend.itemDistance + - i * legendItem.symbol.padding + - textWidths.slice(0, i).reduce((acc, tw) => acc + tw, 0) + acc + + legendItem.symbol.width + + legendItem.symbol.padding + + legendItem.textWidth + + legend.itemDistance ); + }, 0); + }; + + legendItemTemplate + .append('rect') + .attr('x', function (_d, i) { + return getXPosition(i); }) .attr('y', (legendItem) => { return Math.max(0, (legend.lineHeight - legendItem.symbol.height) / 2); @@ -155,17 +158,11 @@ export const Legend = (props: Props) => { .style('fill', function (d) { return d.visible ? d.color : ''; }); + legendItemTemplate .append('text') .attr('x', function (legendItem, i) { - return ( - i * legendItem.symbol.width + - i * legend.itemDistance + - i * legendItem.symbol.padding + - legendItem.symbol.width + - legendItem.symbol.padding + - textWidths.slice(0, i).reduce((acc, tw) => acc + tw, 0) - ); + return getXPosition(i) + legendItem.symbol.width + legendItem.symbol.padding; }) .attr('height', legend.lineHeight) .attr('class', function (d) { @@ -193,7 +190,9 @@ export const Legend = (props: Props) => { if (config.pagination) { const transform = `translate(${[ config.offset.left, - config.offset.top + legend.lineHeight * config.pagination.limit, + config.offset.top + + legend.lineHeight * config.pagination.limit + + legend.lineHeight / 2, ].join(',')})`; appendPaginator({ container: svgElement, diff --git a/src/plugins/d3/renderer/constants/defaults/legend.ts b/src/plugins/d3/renderer/constants/defaults/legend.ts index 359b43a0..680b660e 100644 --- a/src/plugins/d3/renderer/constants/defaults/legend.ts +++ b/src/plugins/d3/renderer/constants/defaults/legend.ts @@ -6,7 +6,7 @@ type LegendDefaults = Required> & export const legendDefaults: LegendDefaults = { align: 'center', itemDistance: 20, - margin: 12, + margin: 15, itemStyle: { fontSize: '12px', }, diff --git a/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts b/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts index 48585ce1..cfac2d2c 100644 --- a/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts +++ b/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts @@ -17,20 +17,6 @@ type Args = { preparedSeries: PreparedSeries[]; }; -const getHeightOccupiedByXAxis = ({preparedXAxis}: {preparedXAxis: PreparedAxis}) => { - let height = 0; - - if (preparedXAxis.title) { - height += preparedXAxis.title.height + preparedXAxis.title.margin; - } - - if (preparedXAxis.labels.enabled) { - height += preparedXAxis.labels.margin + preparedXAxis.labels.height; - } - - return height; -}; - const getBottomOffset = (args: { hasAxisRelatedSeries: boolean; preparedLegend: PreparedLegend; @@ -44,10 +30,14 @@ const getBottomOffset = (args: { } if (hasAxisRelatedSeries) { - result += getHeightOccupiedByXAxis({preparedXAxis}); - } + if (preparedXAxis.title) { + result += preparedXAxis.title.height + preparedXAxis.title.margin; + } - console.log({result, preparedLegend, xAxis: getHeightOccupiedByXAxis({preparedXAxis})}); + if (preparedXAxis.labels.enabled) { + result += preparedXAxis.labels.margin + preparedXAxis.labels.height; + } + } return result; }; diff --git a/src/plugins/d3/renderer/hooks/useChartDimensions/utils.ts b/src/plugins/d3/renderer/hooks/useChartDimensions/utils.ts index 68bff917..48211654 100644 --- a/src/plugins/d3/renderer/hooks/useChartDimensions/utils.ts +++ b/src/plugins/d3/renderer/hooks/useChartDimensions/utils.ts @@ -6,10 +6,28 @@ export const getBoundsWidth = (args: { preparedYAxis: PreparedAxis[]; }) => { const {chartWidth, chartMargin, preparedYAxis} = args; - const yAxisTitleHeight = - preparedYAxis.reduce((acc, axis) => { - return acc + (axis.title.height || 0); - }, 0) || 0; - return chartWidth - chartMargin.right - chartMargin.left - yAxisTitleHeight; + return ( + chartWidth - + chartMargin.right - + chartMargin.left - + getWidthOccupiedByYAxis({preparedAxis: preparedYAxis}) + ); }; + +export function getWidthOccupiedByYAxis(args: {preparedAxis: PreparedAxis[]}) { + const {preparedAxis} = args; + let result = 0; + + preparedAxis.forEach((axis) => { + if (axis.title.text) { + result += axis.title.height + axis.title.margin; + } + + if (axis.labels.enabled) { + result += axis.labels.margin + axis.labels.width; + } + }); + + return result; +} diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts b/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts index 734f4461..315a81d9 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts @@ -2,11 +2,8 @@ import get from 'lodash/get'; import type {ChartKitWidgetData} from '../../../../../types'; -import {isAxisRelatedSeries} from '../../utils'; import type {PreparedAxis, PreparedChart, PreparedTitle} from './types'; -const AXIS_LINE_WIDTH = 1; - const getMarginTop = (args: { chart: ChartKitWidgetData['chart']; preparedTitle?: PreparedTitle; @@ -21,25 +18,6 @@ const getMarginTop = (args: { return marginTop; }; -const getMarginLeft = (args: { - chart: ChartKitWidgetData['chart']; - hasAxisRelatedSeries: boolean; - preparedY1Axis: PreparedAxis; -}) => { - const {chart, hasAxisRelatedSeries, preparedY1Axis} = args; - let marginLeft = get(chart, 'margin.left', 0); - - if (hasAxisRelatedSeries) { - marginLeft += - AXIS_LINE_WIDTH + - preparedY1Axis.labels.margin + - (preparedY1Axis.labels.maxWidth || 0) + - preparedY1Axis.title.height; - } - - return marginLeft; -}; - const getMarginRight = (args: {chart: ChartKitWidgetData['chart']}) => { const {chart} = args; @@ -52,11 +30,10 @@ export const getPreparedChart = (args: { preparedY1Axis: PreparedAxis; preparedTitle?: PreparedTitle; }): PreparedChart => { - const {chart, series, preparedY1Axis, preparedTitle} = args; - const hasAxisRelatedSeries = series.data.some(isAxisRelatedSeries); + const {chart, preparedTitle} = args; const marginTop = getMarginTop({chart, preparedTitle}); const marginBottom = get(chart, 'margin.bottom', 0); - const marginLeft = getMarginLeft({chart, hasAxisRelatedSeries, preparedY1Axis}); + const marginLeft = get(chart, 'margin.left', 0); const marginRight = getMarginRight({chart}); return { diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts index 429692b4..05600803 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts @@ -13,9 +13,9 @@ type PreparedAxisLabels = Omit< > & Required> & { style: BaseTextStyle; - maxWidth?: number; rotation: number; height: number; + width: number; }; export type PreparedChart = { diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts b/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts index 0201d36a..e3c6f6a0 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts @@ -93,6 +93,7 @@ export const getPreparedXAxis = ({ numberFormat: get(xAxis, 'labels.numberFormat'), rotation: get(xAxis, 'labels.rotation', 0), style: {fontSize: get(xAxis, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE)}, + width: 0, height: 0, }, lineColor: get(xAxis, 'lineColor'), diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts b/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts index 1108a5e9..5ee70720 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts @@ -46,7 +46,10 @@ const getAxisLabelMaxWidth = (args: {axis: PreparedAxis; series: ChartKitWidgetS return getLabelsMaxWidth({ labels, - style: axis.labels.style, + style: { + 'font-size': axis.labels.style.fontSize, + 'font-weight': axis.labels.style.fontWeight || '', + }, }); }; @@ -56,7 +59,7 @@ const applyLabelsMaxWidth = (args: { }) => { const {series, preparedYAxis} = args; - preparedYAxis.labels.maxWidth = getAxisLabelMaxWidth({axis: preparedYAxis, series}); + preparedYAxis.labels.width = getAxisLabelMaxWidth({axis: preparedYAxis, series}); }; export const getPreparedYAxis = ({ @@ -86,6 +89,7 @@ export const getPreparedYAxis = ({ numberFormat: get(yAxis1, 'labels.numberFormat'), style: y1LabelsStyle, rotation: 0, + width: 0, height: 0, }, lineColor: get(yAxis1, 'lineColor'), diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts index c9208c2a..caa1f5df 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts @@ -10,6 +10,7 @@ import {getHorisontalSvgTextHeight} from '../../utils'; import {getBoundsWidth} from '../useChartDimensions'; import type {PreparedAxis, PreparedChart} from '../useChartOptions/types'; import type {PreparedLegend, PreparedSeries, LegendConfig, LegendItem} from './types'; +import {getWidthOccupiedByYAxis} from '../useChartDimensions/utils'; type LegendItemWithoutTextWidth = Omit; @@ -116,15 +117,19 @@ export const getLegendComponents = (args: { let pagination: LegendConfig['pagination'] | undefined; if (maxLegendHeight < legendHeight) { - const limit = Math.floor(maxLegendHeight / preparedLegend.lineHeight); + // extra line for paginator + const limit = Math.floor(maxLegendHeight / (preparedLegend.lineHeight + 1)); const maxPage = Math.ceil(items.length / limit); pagination = {limit, maxPage}; legendHeight = maxLegendHeight; } preparedLegend.height = legendHeight; - const top = chartHeight - chartMargin.top - chartMargin.bottom - preparedLegend.height; - const offset: LegendConfig['offset'] = {left: chartMargin.left, top}; + const top = chartHeight - chartMargin.bottom - preparedLegend.height; + const offset: LegendConfig['offset'] = { + left: chartMargin.left + getWidthOccupiedByYAxis({preparedAxis: preparedYAxis}), + top, + }; return {legendConfig: {offset, pagination}, legendItems: items}; }; diff --git a/src/plugins/d3/renderer/utils/axis-generators/bottom.ts b/src/plugins/d3/renderer/utils/axis-generators/bottom.ts index b2eec67c..ef479768 100644 --- a/src/plugins/d3/renderer/utils/axis-generators/bottom.ts +++ b/src/plugins/d3/renderer/utils/axis-generators/bottom.ts @@ -1,8 +1,8 @@ import type {AxisDomain, AxisScale, Selection} from 'd3'; import {select} from 'd3'; import {BaseTextStyle} from '../../../../../types'; -import {getXAxisItems, getXAxisOffset, getXTickPosition, rotateLabels} from '../axis'; -import {setEllipsisForOverflowText} from '../text'; +import {getXAxisItems, getXAxisOffset, getXTickPosition} from '../axis'; +import {getLabelsMaxHeight, setEllipsisForOverflowText} from '../text'; type AxisBottomArgs = { scale: AxisScale; @@ -41,6 +41,11 @@ function addDomain( .attr('d', `M0,0V0H${size}`); } +function calculateCos(deg: number, precision = 4) { + const factor = Math.pow(10, precision); + return Math.floor(Math.cos((Math.PI / 180) * deg) * factor) / factor; +} + export function axisBottom(args: AxisBottomArgs) { const { scale, @@ -57,14 +62,23 @@ export function axisBottom(args: AxisBottomArgs) { domain: {size: domainSize, color: domainColor}, } = args; const offset = getXAxisOffset(); - const spacing = Math.max(tickSize, 0) + labelsMargin; const position = getXTickPosition({scale, offset}); const values = getXAxisItems({scale, count: ticksCount, maxCount: maxTickCount}); + const labelHeight = getLabelsMaxHeight({ + labels: values, + style: {'font-size': labelsStyle?.fontSize || ''}, + }); return function (selection: Selection) { const x = selection.node()?.getBoundingClientRect()?.x || 0; const right = x + domainSize; + let transform = `translate(0, ${labelHeight + labelsMargin}px)`; + if (rotation) { + const labelsOffsetTop = labelHeight * calculateCos(rotation) + labelsMargin; + transform = `translate(0, ${labelsOffsetTop}px) rotate(${rotation}deg)`; + } + selection .selectAll('.tick') .data(values) @@ -73,10 +87,15 @@ export function axisBottom(args: AxisBottomArgs) { const tick = el.append('g').attr('class', 'tick'); tick.append('line').attr('stroke', 'currentColor').attr('y2', tickSize); tick.append('text') + .text(labelFormat) .attr('fill', 'currentColor') - .attr('y', spacing) - .attr('dy', '0.71em') - .text(labelFormat); + .attr('text-anchor', () => { + if (rotation) { + return rotation > 0 ? 'start' : 'end'; + } + return 'middle'; + }) + .style('transform', transform); return tick; }) @@ -96,7 +115,16 @@ export function axisBottom(args: AxisBottomArgs) { const labels = selection.selectAll('.tick text'); if (rotation) { - rotateLabels(labels, {rotation, margin: labelsMargin}); + // labels.attr('text-anchor', rotation > 0 ? 'start' : 'end'); + // .style('transform-box', 'fill-box') + // .style('transform', `rotate(${rotation}deg)`); + // if (rotation < 0) { + // labels.style('transform-origin', function () { + // const labelWidth = (this as Element)?.getBoundingClientRect()?.width || 0; + // return `${labelWidth}px ${labelsMargin}px`; + // }); + // } + // rotateLabels(labels, {rotation, margin: labelsMargin}); } else { // remove overlapping labels let elementX = 0; @@ -143,7 +171,7 @@ export function axisBottom(args: AxisBottomArgs) { selection .call(addDomain, {size: domainSize, color: domainColor}) - .attr('text-anchor', 'middle') + // .attr('text-anchor', 'middle') .style('font-size', labelsStyle?.fontSize || ''); }; } diff --git a/src/plugins/d3/renderer/utils/axis.ts b/src/plugins/d3/renderer/utils/axis.ts index 9f61b4d9..1a0927bc 100644 --- a/src/plugins/d3/renderer/utils/axis.ts +++ b/src/plugins/d3/renderer/utils/axis.ts @@ -1,5 +1,5 @@ import {PreparedAxis} from '../hooks'; -import {AxisDomain, AxisScale, ScaleBand, Selection} from 'd3'; +import {AxisDomain, AxisScale, ScaleBand} from 'd3'; export function getTicksCount({axis, range}: {axis: PreparedAxis; range: number}) { let ticksCount: number | undefined; @@ -64,26 +64,3 @@ export function getMaxTickCount({axis, width}: {axis: PreparedAxis; width: numbe const minTickWidth = parseInt(axis.labels.style.fontSize) + axis.labels.padding; return Math.floor(width / minTickWidth); } - -export function rotateLabels( - selection: Selection, - { - rotation, - margin, - }: { - rotation: number; - margin: number; - }, -) { - selection - .attr('text-anchor', rotation > 0 ? 'start' : 'end') - .style('transform-box', 'fill-box') - .style('transform', `rotate(${rotation}deg)`); - - if (rotation < 0) { - selection.style('transform-origin', function () { - const labelWidth = (this as Element)?.getBoundingClientRect()?.width || 0; - return `${labelWidth}px ${margin}px`; - }); - } -} diff --git a/src/plugins/d3/renderer/utils/text.ts b/src/plugins/d3/renderer/utils/text.ts index 02433a11..504382d8 100644 --- a/src/plugins/d3/renderer/utils/text.ts +++ b/src/plugins/d3/renderer/utils/text.ts @@ -1,7 +1,6 @@ import type {Selection} from 'd3'; import {select} from 'd3'; import {BaseTextStyle} from '../../../../types'; -import {rotateLabels} from './axis'; export function setEllipsisForOverflowText( selection: Selection, @@ -119,7 +118,9 @@ export function getLabelsMaxHeight({ const svg = select(document.body).append('svg'); const textSelection = renderLabels(svg, {labels, style}); if (rotation) { - rotateLabels(textSelection, {rotation, margin: 0}); + textSelection + .attr('text-anchor', rotation > 0 ? 'start' : 'end') + .style('transform', `rotate(${rotation}deg)`); } const maxHeight = (svg.select('g').node() as Element)?.getBoundingClientRect()?.height || 0; From 335bc351cb9fd67e04433fafb9b07d1479033772 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Mon, 25 Sep 2023 20:24:25 +0300 Subject: [PATCH 3/9] add bar-x playground story --- .../__stories__/{ => bar-x}/BarX.stories.tsx | 10 +-- .../__stories__/bar-x/Playground.stories.tsx | 78 +++++++++++++++++++ src/plugins/d3/renderer/components/AxisX.tsx | 5 +- .../renderer/utils/axis-generators/bottom.ts | 26 +++---- 4 files changed, 95 insertions(+), 24 deletions(-) rename src/plugins/d3/__stories__/{ => bar-x}/BarX.stories.tsx (86%) create mode 100644 src/plugins/d3/__stories__/bar-x/Playground.stories.tsx diff --git a/src/plugins/d3/__stories__/BarX.stories.tsx b/src/plugins/d3/__stories__/bar-x/BarX.stories.tsx similarity index 86% rename from src/plugins/d3/__stories__/BarX.stories.tsx rename to src/plugins/d3/__stories__/bar-x/BarX.stories.tsx index dc28cfdc..91098eea 100644 --- a/src/plugins/d3/__stories__/BarX.stories.tsx +++ b/src/plugins/d3/__stories__/bar-x/BarX.stories.tsx @@ -2,15 +2,15 @@ import React from 'react'; import {StoryObj} from '@storybook/react'; import {withKnobs} from '@storybook/addon-knobs'; import {Button} from '@gravity-ui/uikit'; -import {settings} from '../../../libs'; -import {D3Plugin} from '..'; +import {settings} from '../../../../libs'; +import {D3Plugin} from '../..'; import { BasicBarXChart, BasicLinearBarXChart, BasicDateTimeBarXChart, -} from '../examples/bar-x/Basic'; -import {GroupedColumns} from '../examples/bar-x/GroupedColumns'; -import {StackedColumns} from '../examples/bar-x/StackedColumns'; +} from '../../examples/bar-x/Basic'; +import {GroupedColumns} from '../../examples/bar-x/GroupedColumns'; +import {StackedColumns} from '../../examples/bar-x/StackedColumns'; const ChartStory = ({Chart}: {Chart: React.FC}) => { const [shown, setShown] = React.useState(false); diff --git a/src/plugins/d3/__stories__/bar-x/Playground.stories.tsx b/src/plugins/d3/__stories__/bar-x/Playground.stories.tsx new file mode 100644 index 00000000..660e89f2 --- /dev/null +++ b/src/plugins/d3/__stories__/bar-x/Playground.stories.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import {StoryObj} from '@storybook/react'; +import {Button} from '@gravity-ui/uikit'; +import {settings} from '../../../../libs'; +import {D3Plugin} from '../..'; +import {ChartKitWidgetData} from '../../../../types'; +import {ChartKit} from '../../../../components/ChartKit'; +import {groups} from 'd3'; +import nintendoGames from '../../examples/nintendoGames'; + +function prepareData(): ChartKitWidgetData { + const gamesByPlatform = groups(nintendoGames, (item) => item['platform']); + const data = gamesByPlatform.map(([value, games]) => ({ + x: value, + y: games.length, + })); + + return { + series: { + data: [ + { + type: 'bar-x', + data, + name: 'Games released', + }, + ], + }, + xAxis: { + type: 'category', + categories: gamesByPlatform.map(([key]) => key), + title: { + text: 'Game Platforms', + }, + labels: { + enabled: true, + rotation: 30, + }, + }, + yAxis: [{title: {text: 'Number of games released'}}], + }; +} + +const ChartStory = ({data}: {data: ChartKitWidgetData}) => { + const [shown, setShown] = React.useState(false); + + if (!shown) { + settings.set({plugins: [D3Plugin]}); + return ; + } + + return ( +
+ +
+ ); +}; + +export const PlaygroundBarXChartStory: StoryObj = { + name: 'Playground', + args: { + data: prepareData(), + }, + argTypes: { + data: { + control: 'object', + }, + }, +}; + +export default { + title: 'Plugins/D3/Bar-X', + component: ChartStory, +}; diff --git a/src/plugins/d3/renderer/components/AxisX.tsx b/src/plugins/d3/renderer/components/AxisX.tsx index 809f0429..c804e215 100644 --- a/src/plugins/d3/renderer/components/AxisX.tsx +++ b/src/plugins/d3/renderer/components/AxisX.tsx @@ -70,10 +70,7 @@ export const AxisX = React.memo(({axis, width, height, scale}: Props) => { const svgElement = select(ref.current); svgElement.selectAll('*').remove(); - svgElement - .call(xAxisGenerator) - .attr('class', b()) - .style('font-size', axis.labels.style.fontSize); + svgElement.call(xAxisGenerator).attr('class', b()); // add an axis header if necessary if (axis.title.text) { diff --git a/src/plugins/d3/renderer/utils/axis-generators/bottom.ts b/src/plugins/d3/renderer/utils/axis-generators/bottom.ts index ef479768..87aad557 100644 --- a/src/plugins/d3/renderer/utils/axis-generators/bottom.ts +++ b/src/plugins/d3/renderer/utils/axis-generators/bottom.ts @@ -41,11 +41,16 @@ function addDomain( .attr('d', `M0,0V0H${size}`); } -function calculateCos(deg: number, precision = 4) { +function calculateCos(deg: number, precision = 2) { const factor = Math.pow(10, precision); return Math.floor(Math.cos((Math.PI / 180) * deg) * factor) / factor; } +function calculateSin(deg: number, precision = 2) { + const factor = Math.pow(10, precision); + return Math.floor(Math.sin((Math.PI / 180) * deg) * factor) / factor; +} + export function axisBottom(args: AxisBottomArgs) { const { scale, @@ -76,7 +81,9 @@ export function axisBottom(args: AxisBottomArgs) { let transform = `translate(0, ${labelHeight + labelsMargin}px)`; if (rotation) { const labelsOffsetTop = labelHeight * calculateCos(rotation) + labelsMargin; - transform = `translate(0, ${labelsOffsetTop}px) rotate(${rotation}deg)`; + // shift by half the element with middle alignment + const labelsOffsetLeft = (calculateSin(rotation) * labelHeight) / 4; + transform = `translate(${-labelsOffsetLeft}px, ${labelsOffsetTop}px) rotate(${rotation}deg)`; } selection @@ -114,18 +121,8 @@ export function axisBottom(args: AxisBottomArgs) { const labels = selection.selectAll('.tick text'); - if (rotation) { - // labels.attr('text-anchor', rotation > 0 ? 'start' : 'end'); - // .style('transform-box', 'fill-box') - // .style('transform', `rotate(${rotation}deg)`); - // if (rotation < 0) { - // labels.style('transform-origin', function () { - // const labelWidth = (this as Element)?.getBoundingClientRect()?.width || 0; - // return `${labelWidth}px ${labelsMargin}px`; - // }); - // } - // rotateLabels(labels, {rotation, margin: labelsMargin}); - } else { + // FIXME: handle rotated overlapping labels (with a smarter approach) + if (!rotation) { // remove overlapping labels let elementX = 0; selection @@ -171,7 +168,6 @@ export function axisBottom(args: AxisBottomArgs) { selection .call(addDomain, {size: domainSize, color: domainColor}) - // .attr('text-anchor', 'middle') .style('font-size', labelsStyle?.fontSize || ''); }; } From 7ed6ce9c4a1153acb2f1101b6117021b2803fa6b Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Tue, 26 Sep 2023 11:54:49 +0300 Subject: [PATCH 4/9] fix import order --- src/plugins/d3/renderer/components/Chart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/d3/renderer/components/Chart.tsx b/src/plugins/d3/renderer/components/Chart.tsx index f541def6..44bd5d92 100644 --- a/src/plugins/d3/renderer/components/Chart.tsx +++ b/src/plugins/d3/renderer/components/Chart.tsx @@ -18,9 +18,9 @@ import {Legend} from './Legend'; import {Title} from './Title'; import {Tooltip, TooltipTriggerArea} from './Tooltip'; import {getPreparedXAxis} from '../hooks/useChartOptions/x-axis'; +import {getWidthOccupiedByYAxis} from '../hooks/useChartDimensions/utils'; import './styles.scss'; -import {getWidthOccupiedByYAxis} from '../hooks/useChartDimensions/utils'; const b = block('d3'); From 1c4b4d82a7b3fcf8a4cca53dd7d4190a8772af1b Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Tue, 26 Sep 2023 11:59:24 +0300 Subject: [PATCH 5/9] fix types --- .../d3/renderer/hooks/useShapes/bar-x/prepare-data.ts | 6 +++--- src/plugins/d3/renderer/hooks/useShapes/index.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts b/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts index ad789cb5..32d62eab 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts +++ b/src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts @@ -6,7 +6,7 @@ import type {BarXSeriesData, TooltipDataChunkBarX} from '../../../../../../types import {getDataCategoryValue} from '../../../utils'; import type {ChartScale} from '../../useAxisScales'; -import type {ChartOptions} from '../../useChartOptions/types'; +import type {PreparedAxis} from '../../useChartOptions/types'; import type {PreparedBarXSeries, PreparedSeriesOptions} from '../../useSeries/types'; const MIN_RECT_WIDTH = 1; @@ -24,9 +24,9 @@ export type PreparedBarXData = Omit & { export const prepareBarXData = (args: { series: PreparedBarXSeries[]; seriesOptions: PreparedSeriesOptions; - xAxis: ChartOptions['xAxis']; + xAxis: PreparedAxis; xScale: ChartScale; - yAxis: ChartOptions['yAxis']; + yAxis: PreparedAxis[]; yScale: ChartScale; }): PreparedBarXData[] => { const {series, seriesOptions, xAxis, xScale, yScale} = args; diff --git a/src/plugins/d3/renderer/hooks/useShapes/index.tsx b/src/plugins/d3/renderer/hooks/useShapes/index.tsx index 704a2016..04492a2d 100644 --- a/src/plugins/d3/renderer/hooks/useShapes/index.tsx +++ b/src/plugins/d3/renderer/hooks/useShapes/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {Dispatch, group} from 'd3'; import {getOnlyVisibleSeries} from '../../utils'; -import type {ChartOptions} from '../useChartOptions/types'; +import type {PreparedAxis} from '../useChartOptions/types'; import type {ChartScale} from '../useAxisScales'; import type { PreparedBarXSeries, @@ -31,8 +31,8 @@ type Args = { dispatcher: Dispatch; series: PreparedSeries[]; seriesOptions: PreparedSeriesOptions; - xAxis: ChartOptions['xAxis']; - yAxis: ChartOptions['yAxis']; + xAxis: PreparedAxis; + yAxis: PreparedAxis[]; svgContainer: SVGSVGElement | null; xScale?: ChartScale; yScale?: ChartScale; From 777dfb70d79d83853c84d9bd0e1b65b7473ae089 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Tue, 26 Sep 2023 14:12:30 +0300 Subject: [PATCH 6/9] fix label margin --- src/plugins/d3/renderer/components/styles.scss | 1 + src/plugins/d3/renderer/constants/defaults/axis.ts | 4 ++-- .../d3/renderer/hooks/useChartOptions/types.ts | 2 +- .../d3/renderer/hooks/useChartOptions/y-axis.ts | 11 +++++++---- .../d3/renderer/utils/axis-generators/bottom.ts | 6 +++--- src/plugins/d3/renderer/utils/text.ts | 2 +- src/types/widget-data/axis.ts | 6 +++++- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/plugins/d3/renderer/components/styles.scss b/src/plugins/d3/renderer/components/styles.scss index 4288f821..02f894b0 100644 --- a/src/plugins/d3/renderer/components/styles.scss +++ b/src/plugins/d3/renderer/components/styles.scss @@ -12,6 +12,7 @@ } &__title { + alignment-baseline: after-edge; fill: var(--g-color-text-secondary); } } diff --git a/src/plugins/d3/renderer/constants/defaults/axis.ts b/src/plugins/d3/renderer/constants/defaults/axis.ts index 94ecfc29..96e4fcd6 100644 --- a/src/plugins/d3/renderer/constants/defaults/axis.ts +++ b/src/plugins/d3/renderer/constants/defaults/axis.ts @@ -10,10 +10,10 @@ const axisTitleDefaults = { export const xAxisTitleDefaults = { ...axisTitleDefaults, - margin: 0, + margin: 4, }; export const yAxisTitleDefaults = { ...axisTitleDefaults, - margin: 10, + margin: 8, }; diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts index 05600803..2cf7d1b5 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts @@ -5,7 +5,7 @@ import type { ChartKitWidgetAxisType, ChartKitWidgetAxisLabels, ChartMargin, -} from '../../../../../types/widget-data'; +} from '../../../../../types'; type PreparedAxisLabels = Omit< ChartKitWidgetAxisLabels, diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts b/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts index 5ee70720..911ba73d 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts @@ -71,6 +71,7 @@ export const getPreparedYAxis = ({ }): PreparedAxis[] => { // FIXME: add support for n axises const yAxis1 = yAxis?.[0]; + const labelsEnabled = get(yAxis1, 'labels.enabled', true); const y1LabelsStyle: BaseTextStyle = { fontSize: get(yAxis1, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE), @@ -82,9 +83,9 @@ export const getPreparedYAxis = ({ const preparedY1Axis: PreparedAxis = { type: get(yAxis1, 'type', 'linear'), labels: { - enabled: get(yAxis1, 'labels.enabled', true), - margin: get(yAxis1, 'labels.margin', axisLabelsDefaults.margin), - padding: get(yAxis1, 'labels.padding', axisLabelsDefaults.padding), + enabled: labelsEnabled, + margin: labelsEnabled ? get(yAxis1, 'labels.margin', axisLabelsDefaults.margin) : 0, + padding: labelsEnabled ? get(yAxis1, 'labels.padding', axisLabelsDefaults.padding) : 0, dateFormat: get(yAxis1, 'labels.dateFormat'), numberFormat: get(yAxis1, 'labels.numberFormat'), style: y1LabelsStyle, @@ -113,7 +114,9 @@ export const getPreparedYAxis = ({ }, }; - applyLabelsMaxWidth({series, preparedYAxis: preparedY1Axis}); + if (labelsEnabled) { + applyLabelsMaxWidth({series, preparedYAxis: preparedY1Axis}); + } return [preparedY1Axis]; }; diff --git a/src/plugins/d3/renderer/utils/axis-generators/bottom.ts b/src/plugins/d3/renderer/utils/axis-generators/bottom.ts index 87aad557..63ec83ed 100644 --- a/src/plugins/d3/renderer/utils/axis-generators/bottom.ts +++ b/src/plugins/d3/renderer/utils/axis-generators/bottom.ts @@ -81,8 +81,7 @@ export function axisBottom(args: AxisBottomArgs) { let transform = `translate(0, ${labelHeight + labelsMargin}px)`; if (rotation) { const labelsOffsetTop = labelHeight * calculateCos(rotation) + labelsMargin; - // shift by half the element with middle alignment - const labelsOffsetLeft = (calculateSin(rotation) * labelHeight) / 4; + const labelsOffsetLeft = calculateSin(rotation) * labelHeight; transform = `translate(${-labelsOffsetLeft}px, ${labelsOffsetTop}px) rotate(${rotation}deg)`; } @@ -102,7 +101,8 @@ export function axisBottom(args: AxisBottomArgs) { } return 'middle'; }) - .style('transform', transform); + .style('transform', transform) + .style('alignment-baseline', 'after-edge'); return tick; }) diff --git a/src/plugins/d3/renderer/utils/text.ts b/src/plugins/d3/renderer/utils/text.ts index 504382d8..b1b356cf 100644 --- a/src/plugins/d3/renderer/utils/text.ts +++ b/src/plugins/d3/renderer/utils/text.ts @@ -8,7 +8,7 @@ export function setEllipsisForOverflowText( ) { let text = selection.text(); selection.text(null).append('title').text(text); - const tSpan = selection.append('tspan').text(text); + const tSpan = selection.append('tspan').text(text).style('alignment-baseline', 'inherit'); let textLength = tSpan.node()?.getComputedTextLength() || 0; while (textLength > maxWidth && text.length > 1) { diff --git a/src/types/widget-data/axis.ts b/src/types/widget-data/axis.ts index 7a5a3ac9..deb02a1b 100644 --- a/src/types/widget-data/axis.ts +++ b/src/types/widget-data/axis.ts @@ -23,6 +23,10 @@ export type ChartKitWidgetAxisLabels = { * If there is enough space, labels are not rotated. * As the chart gets narrower, it will start rotating the labels -45 degrees. */ autoRotation?: boolean; + /** Rotation of the labels in degrees. + * + * @default: 0 + */ rotation?: number; }; @@ -38,7 +42,7 @@ export type ChartKitWidgetAxis = { text?: string; /** The pixel distance between the axis labels or line and the title. * - * Defaults to 4 for horizontal axes, 10 for vertical. + * Defaults to 4 for horizontal axes, 8 for vertical. * */ margin?: number; }; From acc5fdbbe5e320fc95e68f4a710e645f1258c2c4 Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Tue, 26 Sep 2023 15:06:41 +0300 Subject: [PATCH 7/9] move prepare Y Axis --- src/plugins/d3/renderer/components/Chart.tsx | 9 ++++++++- src/plugins/d3/renderer/hooks/useChartOptions/chart.ts | 4 +--- src/plugins/d3/renderer/hooks/useChartOptions/index.ts | 10 +++------- src/plugins/d3/renderer/hooks/useChartOptions/types.ts | 1 - 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/plugins/d3/renderer/components/Chart.tsx b/src/plugins/d3/renderer/components/Chart.tsx index 44bd5d92..9020b810 100644 --- a/src/plugins/d3/renderer/components/Chart.tsx +++ b/src/plugins/d3/renderer/components/Chart.tsx @@ -19,6 +19,7 @@ import {Title} from './Title'; import {Tooltip, TooltipTriggerArea} from './Tooltip'; import {getPreparedXAxis} from '../hooks/useChartOptions/x-axis'; import {getWidthOccupiedByYAxis} from '../hooks/useChartDimensions/utils'; +import {getPreparedYAxis} from '../hooks/useChartOptions/y-axis'; import './styles.scss'; @@ -39,13 +40,19 @@ export const Chart = (props: Props) => { const dispatcher = React.useMemo(() => { return getD3Dispatcher(); }, []); - const {chart, title, tooltip, yAxis} = useChartOptions({ + const {chart, title, tooltip} = useChartOptions({ data, }); const xAxis = React.useMemo( () => getPreparedXAxis({xAxis: data.xAxis, width, series: data.series.data}), [data, width], ); + + const yAxis = React.useMemo( + () => getPreparedYAxis({series: data.series.data, yAxis: data.yAxis}), + [data, width], + ); + const { legendItems, legendConfig, diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts b/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts index 315a81d9..8b71051f 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/chart.ts @@ -2,7 +2,7 @@ import get from 'lodash/get'; import type {ChartKitWidgetData} from '../../../../../types'; -import type {PreparedAxis, PreparedChart, PreparedTitle} from './types'; +import type {PreparedChart, PreparedTitle} from './types'; const getMarginTop = (args: { chart: ChartKitWidgetData['chart']; @@ -26,8 +26,6 @@ const getMarginRight = (args: {chart: ChartKitWidgetData['chart']}) => { export const getPreparedChart = (args: { chart: ChartKitWidgetData['chart']; - series: ChartKitWidgetData['series']; - preparedY1Axis: PreparedAxis; preparedTitle?: PreparedTitle; }): PreparedChart => { const {chart, preparedTitle} = args; diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/index.ts b/src/plugins/d3/renderer/hooks/useChartOptions/index.ts index 506d5716..e4105b37 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/index.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/index.ts @@ -5,7 +5,6 @@ import type {ChartKitWidgetData} from '../../../../../types'; import {getPreparedChart} from './chart'; import {getPreparedTitle} from './title'; import {getPreparedTooltip} from './tooltip'; -import {getPreparedYAxis} from './y-axis'; import type {ChartOptions} from './types'; type Args = { @@ -14,25 +13,22 @@ type Args = { export const useChartOptions = (args: Args): ChartOptions => { const { - data: {chart, series, title, tooltip, yAxis}, + data: {chart, title, tooltip}, } = args; const options: ChartOptions = React.useMemo(() => { const preparedTitle = getPreparedTitle({title}); const preparedTooltip = getPreparedTooltip({tooltip}); - const preparedYAxis = getPreparedYAxis({series: series.data, yAxis}); + const preparedChart = getPreparedChart({ chart, - series, preparedTitle, - preparedY1Axis: preparedYAxis[0], }); return { chart: preparedChart, title: preparedTitle, tooltip: preparedTooltip, - yAxis: preparedYAxis, }; - }, [chart, title, tooltip, series, yAxis]); + }, [chart, title, tooltip]); return options; }; diff --git a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts index 2cf7d1b5..dd32ebbf 100644 --- a/src/plugins/d3/renderer/hooks/useChartOptions/types.ts +++ b/src/plugins/d3/renderer/hooks/useChartOptions/types.ts @@ -52,6 +52,5 @@ export type PreparedTooltip = ChartKitWidgetData['tooltip'] & { export type ChartOptions = { chart: PreparedChart; tooltip: PreparedTooltip; - yAxis: PreparedAxis[]; title?: PreparedTitle; }; From 347e992aa100b81d205bca06c15af545156d54dd Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Tue, 26 Sep 2023 15:08:23 +0300 Subject: [PATCH 8/9] fix type --- src/plugins/d3/renderer/hooks/useAxisScales/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/d3/renderer/hooks/useAxisScales/index.ts b/src/plugins/d3/renderer/hooks/useAxisScales/index.ts index 25cbefd7..fdc34b99 100644 --- a/src/plugins/d3/renderer/hooks/useAxisScales/index.ts +++ b/src/plugins/d3/renderer/hooks/useAxisScales/index.ts @@ -3,7 +3,7 @@ import {scaleBand, scaleLinear, scaleUtc, extent} from 'd3'; import type {ScaleBand, ScaleLinear, ScaleTime} from 'd3'; import get from 'lodash/get'; -import type {ChartOptions, PreparedAxis} from '../useChartOptions/types'; +import type {PreparedAxis} from '../useChartOptions/types'; import { getOnlyVisibleSeries, getDataCategoryValue, @@ -26,7 +26,7 @@ type Args = { boundsHeight: number; series: PreparedSeries[]; xAxis: PreparedAxis; - yAxis: ChartOptions['yAxis']; + yAxis: PreparedAxis[]; }; type ReturnValue = { From 075a1f2b304d5a2b929b506682b8dc8071f9beaf Mon Sep 17 00:00:00 2001 From: "Irina V. Kuzmina" Date: Tue, 26 Sep 2023 17:59:31 +0300 Subject: [PATCH 9/9] fix legend --- src/plugins/d3/renderer/components/Legend.tsx | 5 ++--- src/plugins/d3/renderer/components/styles.scss | 1 + src/plugins/d3/renderer/hooks/useChartDimensions/index.ts | 2 +- src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/d3/renderer/components/Legend.tsx b/src/plugins/d3/renderer/components/Legend.tsx index 5d9b0e85..60274ae5 100644 --- a/src/plugins/d3/renderer/components/Legend.tsx +++ b/src/plugins/d3/renderer/components/Legend.tsx @@ -145,7 +145,7 @@ export const Legend = (props: Props) => { return getXPosition(i); }) .attr('y', (legendItem) => { - return Math.max(0, (legend.lineHeight - legendItem.symbol.height) / 2); + return (legend.lineHeight - legendItem.symbol.height) / 2; }) .attr('width', (legendItem) => { return legendItem.symbol.width; @@ -172,8 +172,7 @@ export const Legend = (props: Props) => { .text(function (d) { return ('name' in d && d.name) as string; }) - .style('font-size', legend.itemStyle.fontSize) - .style('alignment-baseline', 'before-edge'); + .style('font-size', legend.itemStyle.fontSize); const contentWidth = legendLine.node()?.getBoundingClientRect().width || 0; const {left} = getLegendPosition({ diff --git a/src/plugins/d3/renderer/components/styles.scss b/src/plugins/d3/renderer/components/styles.scss index 02f894b0..dd37ae6a 100644 --- a/src/plugins/d3/renderer/components/styles.scss +++ b/src/plugins/d3/renderer/components/styles.scss @@ -33,6 +33,7 @@ &__item-text { fill: var(--g-color-text-secondary); + alignment-baseline: before-edge; &_unselected { fill: var(--g-color-text-hint); diff --git a/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts b/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts index cfac2d2c..fcbb003a 100644 --- a/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts +++ b/src/plugins/d3/renderer/hooks/useChartDimensions/index.ts @@ -30,7 +30,7 @@ const getBottomOffset = (args: { } if (hasAxisRelatedSeries) { - if (preparedXAxis.title) { + if (preparedXAxis.title.text) { result += preparedXAxis.title.height + preparedXAxis.title.margin; } diff --git a/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts b/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts index caa1f5df..1776e436 100644 --- a/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts +++ b/src/plugins/d3/renderer/hooks/useSeries/prepare-legend.ts @@ -118,7 +118,7 @@ export const getLegendComponents = (args: { if (maxLegendHeight < legendHeight) { // extra line for paginator - const limit = Math.floor(maxLegendHeight / (preparedLegend.lineHeight + 1)); + const limit = Math.floor(maxLegendHeight / preparedLegend.lineHeight) - 1; const maxPage = Math.ceil(items.length / limit); pagination = {limit, maxPage}; legendHeight = maxLegendHeight;