Skip to content

Commit

Permalink
feat: implement labelFlush behavior for continuous axes (#117)
Browse files Browse the repository at this point in the history
* feat: add labelFlush to definition

* feat: implement flushing
  • Loading branch information
kristw authored and zhaoyongjie committed Nov 26, 2021
1 parent 2333030 commit c691415
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LineChartPlugin as LegacyLineChartPlugin } from '../../../../../superset-ui-preset-chart-xy/src/legacy';
import { LineChartPlugin } from '../../../../../superset-ui-preset-chart-xy/src';
import BasicStories from './stories/basic';
import FlushStories from './stories/flush';
import QueryStories from './stories/query';
import LegacyStories from './stories/legacy';
import MissingStories from './stories/missing';
Expand All @@ -13,6 +14,7 @@ new LineChartPlugin().configure({ key: LINE_PLUGIN_TYPE }).register();
export default {
examples: [
...BasicStories,
...FlushStories,
...MissingStories,
...TimeShiftStories,
...LegacyStories,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/* eslint-disable no-magic-numbers, sort-keys */
import * as React from 'react';
import { SuperChart, ChartProps } from '@superset-ui/chart';
import { radios } from '@storybook/addon-knobs';
import rawData from '../data/data';
import { LINE_PLUGIN_TYPE } from '../constants';

const MIN_TIME = new Date(Date.UTC(1980, 0, 1)).getTime();
const MAX_TIME = new Date(Date.UTC(2000, 1, 1)).getTime();
const data = rawData.filter(({ x }) => x >= MIN_TIME && x <= MAX_TIME);

export default [
{
renderStory: () => [
<SuperChart
key="line1"
chartType={LINE_PLUGIN_TYPE}
chartProps={
new ChartProps({
datasource: { verboseMap: {} },
formData: {
encoding: {
x: {
field: 'x',
type: 'temporal',
format: '%Y',
scale: {
type: 'utc',
},
axis: {
tickCount: 6,
orient: radios('x.axis.orient', { top: 'top', bottom: 'bottom' }, 'bottom'),
title: radios(
'x.axis.title',
{ enable: 'Time', disable: '', '': undefined },
'Time',
),
},
},
y: {
field: 'y',
type: 'quantitative',
scale: {
type: 'linear',
},
axis: {
tickCount: 3,
orient: radios(
'y.axis.orient',
{ left: 'left', right: 'right', '': undefined },
'left',
),
title: radios(
'y.axis.title',
{ enable: 'Score', disable: '', '': undefined },
'Score',
),
},
},
stroke: {
field: 'name',
type: 'nominal',
legend: true,
},
},
},
height: 200,
payload: { data },
width: 400,
})
}
/>,
<SuperChart
key="line1"
chartType={LINE_PLUGIN_TYPE}
chartProps={
new ChartProps({
datasource: { verboseMap: {} },
formData: {
encoding: {
x: {
field: 'x',
type: 'temporal',
format: '%Y',
scale: {
type: 'utc',
},
axis: {
labelFlush: 5,
tickCount: 6,
orient: radios('x.axis.orient', { top: 'top', bottom: 'bottom' }, 'bottom'),
title: radios(
'x.axis.title',
{ enable: 'Time', disable: '', '': undefined },
'Time',
),
},
},
y: {
field: 'y',
type: 'quantitative',
scale: {
type: 'linear',
},
axis: {
tickCount: 3,
orient: radios(
'y.axis.orient',
{ left: 'left', right: 'right', '': undefined },
'left',
),
title: radios(
'y.axis.title',
{ enable: 'Score', disable: '', '': undefined },
'Score',
),
},
},
stroke: {
field: 'name',
type: 'nominal',
legend: true,
},
},
},
height: 200,
payload: { data },
width: 400,
})
}
/>,
],
storyName: 'with labelFlush',
storyPath: 'preset-chart-xy|LineChartPlugin',
},
];
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-magic-numbers */
import { CSSProperties } from 'react';
import { Value } from 'vega-lite/build/src/channeldef';
import { getTextDimension, Margin } from '@superset-ui/dimension';
import { getTextDimension, Margin, Dimension } from '@superset-ui/dimension';
import { CategoricalColorScale } from '@superset-ui/color';
import { extractFormatFromTypeAndFormat } from './parsers/extractFormat';
import { CoreAxis, LabelOverlapStrategy, AxisOrient } from './types/Axis';
Expand Down Expand Up @@ -31,6 +31,19 @@ const DEFAULT_Y_CONFIG: CoreAxis = {
orient: 'left',
};

export interface AxisLayout {
axisWidth: number;
labelAngle: number;
labelFlush: number | boolean;
labelOffset: number;
labelOverlap: 'flat' | 'rotate';
minMargin: Partial<Margin>;
orient: AxisOrient;
tickLabelDimensions: Dimension[];
tickLabels: string[];
tickTextAnchor?: string;
}

export default class AxisAgent<Def extends ChannelDef<Output>, Output extends Value = Value> {
private readonly channelEncoder: ChannelEncoder<Def, Output>;
private readonly format?: (value: any) => string;
Expand Down Expand Up @@ -108,17 +121,10 @@ export default class AxisAgent<Def extends ChannelDef<Output>, Output extends Va
labelAngle?: number;
tickLength?: number;
tickTextStyle?: CSSProperties;
}): {
labelAngle: number;
labelOffset: number;
labelOverlap: 'flat' | 'rotate';
minMargin: Partial<Margin>;
orient: AxisOrient;
tickTextAnchor?: string;
} {
}): AxisLayout {
const tickLabels = this.getTickLabels();

const labelDimensions = tickLabels.map((text: string) =>
const tickLabelDimensions = tickLabels.map((text: string) =>
getTextDimension({
style: tickTextStyle,
text,
Expand All @@ -127,7 +133,7 @@ export default class AxisAgent<Def extends ChannelDef<Output>, Output extends Va

const { labelOverlap, labelPadding, orient } = this.config;

const maxWidth = Math.max(...labelDimensions.map(d => d.width), 0);
const maxWidth = Math.max(...tickLabelDimensions.map(d => d.width), 0);

// TODO: Add other strategies: stagger, chop, wrap.
let strategyForLabelOverlap = labelOverlap;
Expand All @@ -149,7 +155,7 @@ export default class AxisAgent<Def extends ChannelDef<Output>, Output extends Va

if (this.channelEncoder.isX()) {
if (strategyForLabelOverlap === 'flat') {
const labelHeight = labelDimensions.length > 0 ? labelDimensions[0].height : 0;
const labelHeight = tickLabelDimensions.length > 0 ? tickLabelDimensions[0].height : 0;
labelOffset = labelHeight + labelPadding;
requiredMargin += labelHeight;
} else if (strategyForLabelOverlap === 'rotate') {
Expand All @@ -168,13 +174,21 @@ export default class AxisAgent<Def extends ChannelDef<Output>, Output extends Va
}

return {
axisWidth,
labelAngle: strategyForLabelOverlap === 'flat' ? 0 : labelAngle,
labelFlush:
typeof this.config.labelFlush === 'undefined'
? // If not set, only enable flushing for continuous scales
this.channelEncoder.scale!.scaleTypeCategory === 'continuous'
: this.config.labelFlush,
labelOffset,
labelOverlap: strategyForLabelOverlap,
minMargin: {
[orient]: Math.ceil(requiredMargin),
},
orient,
tickLabelDimensions,
tickLabels,
tickTextAnchor,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface ScaleAgent<Output extends Value> {
| ScaleOrdinal<{ toString(): string }, Output>
| ScalePoint<{ toString(): string }>
| ScaleBand<{ toString(): string }>;
scaleTypeCategory: 'continuous' | 'discrete' | 'discretizing';
}

export interface ScaleTypeToD3ScaleType<Output> {
Expand Down Expand Up @@ -199,11 +200,26 @@ function createScale<Output extends Value>(
return scale;
}

const continuousScaleTypes = new Set(['linear', 'pow', 'sqrt', 'symlog', 'log', 'time', 'utc']);
const discreteScaleTypes = new Set(['band', 'point']);
const discretizingScaleTypes = new Set(['bin-ordinal', 'quantile', 'quantize', 'threshold']);

function getScaleTypeCategory(scaleType: ScaleType) {
if (continuousScaleTypes.has(scaleType)) {
return 'continuous';
}
if (discreteScaleTypes.has(scaleType)) {
return 'discrete';
}

return 'discretizing';
}

export default function extractScale<Output extends Value>(
channelType: ChannelType,
definition: ChannelDef<Output>,
namespace?: string,
) {
): ScaleAgent<Output> | undefined {
if (isNonValueDef(definition)) {
const scaleConfig =
'scale' in definition && typeof definition.scale !== 'undefined' ? definition.scale : {};
Expand Down Expand Up @@ -240,6 +256,7 @@ export default function extractScale<Output extends Value>(
value: number | string | boolean | null | undefined | Date,
) => Output,
scale,
scaleTypeCategory: getScaleTypeCategory(scaleType),
setDomain,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** See https://vega.github.io/vega-lite/docs/axis.html */

import { DateTime } from 'vega-lite/build/src/datetime';

export type AxisOrient = 'top' | 'bottom' | 'left' | 'right';
Expand All @@ -7,6 +9,15 @@ export type LabelOverlapStrategy = 'auto' | 'flat' | 'rotate';
export interface CoreAxis {
format?: string;
labelAngle: number;
/**
* Indicates if the first and last axis labels should be aligned flush with the scale range.
* Flush alignment for a horizontal axis will left-align the first label and right-align the last label.
* For vertical axes, bottom and top text baselines are applied instead.
* If this property is a number, it also indicates the number of pixels by which to offset the first and last labels;
* for example, a value of 2 will flush-align the first and last labels
* and also push them 2 pixels outward from the center of the axis.
* The additional adjustment can sometimes help the labels better visually group with corresponding axis ticks. */
labelFlush?: boolean | number;
labelOverlap: LabelOverlapStrategy;
/** The padding, in pixels, between axis and text labels. */
labelPadding: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { Margin, mergeMargin, Dimension } from '@superset-ui/dimension';
import { ChartFrame } from '@superset-ui/chart-composition';
import createTickComponent from './createTickComponent';
import ChannelEncoder from '../encodeable/ChannelEncoder';
import { AxisOrient } from '../encodeable/types/Axis';
import { XFieldDef, YFieldDef } from '../encodeable/types/ChannelDef';
import { PlainObject } from '../encodeable/types/Data';
import { DEFAULT_LABEL_ANGLE } from './constants';
import convertScaleToDataUIScale from './convertScaleToDataUIScaleShape';
import { AxisLayout } from '../encodeable/AxisAgent';

// Additional margin to avoid content hidden behind scroll bar
const OVERFLOW_MARGIN = 8;
Expand All @@ -37,20 +37,9 @@ export default class XYChartLayout {
margin: Margin;
config: XYChartLayoutConfig;

xLayout?: {
labelOffset: number;
labelOverlap: string;
labelAngle: number;
tickTextAnchor?: string;
minMargin: Partial<Margin>;
orient: AxisOrient;
};

yLayout?: {
labelOffset: number;
minMargin: Partial<Margin>;
orient: AxisOrient;
};
xLayout?: AxisLayout;

yLayout?: AxisLayout;

// eslint-disable-next-line complexity
constructor(config: XYChartLayoutConfig) {
Expand Down

0 comments on commit c691415

Please sign in to comment.