mirror of
https://github.com/apache/superset.git
synced 2024-09-19 20:19:37 -04:00
feat: implement labelFlush behavior for continuous axes (#117)
* feat: add labelFlush to definition * feat: implement flushing
This commit is contained in:
parent
2333030abf
commit
c691415702
@ -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';
|
||||
@ -13,6 +14,7 @@ new LineChartPlugin().configure({ key: LINE_PLUGIN_TYPE }).register();
|
||||
export default {
|
||||
examples: [
|
||||
...BasicStories,
|
||||
...FlushStories,
|
||||
...MissingStories,
|
||||
...TimeShiftStories,
|
||||
...LegacyStories,
|
||||
|
@ -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',
|
||||
},
|
||||
];
|
@ -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';
|
||||
@ -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;
|
||||
@ -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,
|
||||
@ -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;
|
||||
@ -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') {
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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> {
|
||||
@ -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 : {};
|
||||
@ -240,6 +256,7 @@ export default function extractScale<Output extends Value>(
|
||||
value: number | string | boolean | null | undefined | Date,
|
||||
) => Output,
|
||||
scale,
|
||||
scaleTypeCategory: getScaleTypeCategory(scaleType),
|
||||
setDomain,
|
||||
};
|
||||
}
|
||||
|
@ -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';
|
||||
@ -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;
|
||||
|
@ -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;
|
||||
@ -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;
|
||||
};
|
||||
xLayout?: AxisLayout;
|
||||
|
||||
yLayout?: {
|
||||
labelOffset: number;
|
||||
minMargin: Partial<Margin>;
|
||||
orient: AxisOrient;
|
||||
};
|
||||
yLayout?: AxisLayout;
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
constructor(config: XYChartLayoutConfig) {
|
||||
|
@ -1,18 +1,19 @@
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { Dimension } from '@superset-ui/dimension';
|
||||
import { AxisLayout } from '../encodeable/AxisAgent';
|
||||
|
||||
export default function createTickComponent({
|
||||
axisWidth,
|
||||
labelAngle,
|
||||
labelFlush,
|
||||
labelOverlap,
|
||||
orient,
|
||||
tickTextAnchor = 'start',
|
||||
}: {
|
||||
labelAngle: number;
|
||||
labelOverlap: string;
|
||||
orient: string;
|
||||
tickTextAnchor?: string;
|
||||
}) {
|
||||
tickLabels,
|
||||
tickLabelDimensions,
|
||||
tickTextAnchor = 'middle',
|
||||
}: AxisLayout) {
|
||||
if (labelOverlap === 'rotate' && labelAngle !== 0) {
|
||||
let xOffset = labelAngle > 0 ? -6 : 6;
|
||||
if (orient === 'top') {
|
||||
@ -20,7 +21,7 @@ export default function createTickComponent({
|
||||
}
|
||||
const yOffset = orient === 'top' ? -3 : 0;
|
||||
|
||||
const TickComponent = ({
|
||||
return ({
|
||||
x,
|
||||
y,
|
||||
dy,
|
||||
@ -39,8 +40,50 @@ export default function createTickComponent({
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
return TickComponent;
|
||||
if (labelFlush === true || typeof labelFlush === 'number') {
|
||||
const labelToDimensionMap = new Map<string, Dimension>();
|
||||
tickLabels.forEach((label, i) => {
|
||||
labelToDimensionMap.set(label, tickLabelDimensions[i]);
|
||||
});
|
||||
|
||||
return ({
|
||||
x,
|
||||
y,
|
||||
dy,
|
||||
formattedValue = '',
|
||||
...textStyle
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
dy?: number;
|
||||
formattedValue: string;
|
||||
textStyle: CSSProperties;
|
||||
}) => {
|
||||
const dimension = labelToDimensionMap.get(formattedValue);
|
||||
const labelWidth = typeof dimension === 'undefined' ? 0 : dimension.width;
|
||||
let textAnchor = tickTextAnchor;
|
||||
let xOffset = 0;
|
||||
|
||||
if (x - labelWidth / 2 < 0) {
|
||||
textAnchor = 'start';
|
||||
if (typeof labelFlush === 'number') {
|
||||
xOffset -= labelFlush;
|
||||
}
|
||||
} else if (x + labelWidth / 2 > axisWidth) {
|
||||
textAnchor = 'end';
|
||||
if (typeof labelFlush === 'number') {
|
||||
xOffset += labelFlush;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<text x={x + xOffset} y={y} {...textStyle} textAnchor={textAnchor}>
|
||||
{formattedValue}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// This will render the tick as horizontal string.
|
||||
|
Loading…
Reference in New Issue
Block a user