Skip to content

Commit

Permalink
feat: allow legend overrides at multiple levels (apache-superset#81)
Browse files Browse the repository at this point in the history
* feat: reduce legend text size

* feat: compute legend data

* feat: support multi-level overrides for legend
  • Loading branch information
kristw committed May 7, 2019
1 parent 5f21ffd commit 034eda1
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import XYChartLayout from '../utils/XYChartLayout';
import WithLegend from '../components/WithLegend';
import Encoder, { ChannelTypes, Encoding, Outputs } from './Encoder';
import { Dataset, PlainObject } from '../encodeable/types/Data';
import ChartLegend from '../components/ChartLegend';
import ChartLegend from '../components/legend/ChartLegend';

chartTheme.gridStyles.stroke = '#f1f3f5';

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { CSSProperties, PureComponent } from 'react';
import { Value } from 'vega-lite/build/src/channeldef';
import AbstractEncoder from '../../encodeable/AbstractEncoder';
import { Dataset } from '../../encodeable/types/Data';
import { ObjectWithKeysFromAndValueType } from '../../encodeable/types/Base';
import { ChannelType, EncodingFromChannelsAndOutputs } from '../../encodeable/types/Channel';
import { BaseOptions } from '../../encodeable/types/Specification';
import {
LegendItemRendererType,
LegendGroupRendererType,
LegendItemLabelRendererType,
LegendItemMarkRendererType,
} from './types';
import DefaultLegendGroup from './DefaultLegendGroup';

const LEGEND_CONTAINER_STYLE: CSSProperties = {
maxHeight: 100,
overflowY: 'auto',
paddingLeft: 14,
paddingTop: 6,
position: 'relative',
};

type Props<Encoder, ChannelTypes> = {
data: Dataset;
encoder: Encoder;
LegendGroupRenderer?: LegendGroupRendererType<ChannelTypes>;
LegendItemRenderer?: LegendItemRendererType<ChannelTypes>;
LegendItemMarkRenderer?: LegendItemMarkRendererType<ChannelTypes>;
LegendItemLabelRenderer?: LegendItemLabelRendererType<ChannelTypes>;
};

export default class ChartLegend<
ChannelTypes extends ObjectWithKeysFromAndValueType<Outputs, ChannelType>,
Outputs extends ObjectWithKeysFromAndValueType<Encoding, Value>,
Encoding extends EncodingFromChannelsAndOutputs<
ChannelTypes,
Outputs
> = EncodingFromChannelsAndOutputs<ChannelTypes, Outputs>,
Options extends BaseOptions = BaseOptions
> extends PureComponent<
Props<AbstractEncoder<ChannelTypes, Outputs, Encoding, Options>, ChannelTypes>,
{}
> {
render() {
const {
data,
encoder,
LegendGroupRenderer,
LegendItemRenderer,
LegendItemMarkRenderer,
LegendItemLabelRenderer,
} = this.props;

const LegendGroup =
typeof LegendGroupRenderer === 'undefined' ? DefaultLegendGroup : LegendGroupRenderer;

return (
<div style={LEGEND_CONTAINER_STYLE}>
{encoder.getLegendInfos(data).map(items => (
<LegendGroup
key={items[0].field}
items={items}
ItemRenderer={LegendItemRenderer}
ItemMarkRenderer={LegendItemMarkRenderer}
ItemLabelRenderer={LegendItemLabelRenderer}
/>
))}
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { CSSProperties } from 'react';
import { LegendGroupRendererProps } from './types';
import DefaultLegendItem from './DefaultLegendItem';

const LEGEND_GROUP_STYLE: CSSProperties = {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
fontSize: '0.8em',
};

export default function DefaultLegendGroupRenderer<ChannelTypes>({
items,
ItemRenderer,
ItemMarkRenderer,
ItemLabelRenderer,
}: LegendGroupRendererProps<ChannelTypes>) {
const LegendItem = typeof ItemRenderer === 'undefined' ? DefaultLegendItem : ItemRenderer;

return (
<div style={LEGEND_GROUP_STYLE}>
{items.map(item => (
<LegendItem
key={`legend-item-${item.field}-${item.value}`}
item={item}
MarkRenderer={ItemMarkRenderer}
LabelRenderer={ItemLabelRenderer}
/>
))}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { CSSProperties } from 'react';
import { LegendItem, LegendLabel } from '@vx/legend';
import { LegendItemRendererProps } from './types';

const MARK_SIZE = 8;

const MARK_STYLE: CSSProperties = { display: 'inline-block' };

export default function DefaultLegendItem<ChannelTypes>({
item,
MarkRenderer,
LabelRenderer,
}: LegendItemRendererProps<ChannelTypes>) {
return (
<LegendItem key={`legend-item-${item.field}-${item.value}`} margin="0 5px">
{typeof MarkRenderer === 'undefined' ? (
<svg width={MARK_SIZE} height={MARK_SIZE} style={MARK_STYLE}>
<circle
fill={
// @ts-ignore
item.encodedValues.color || item.encodedValues.fill || '#ccc'
}
stroke={
// @ts-ignore
item.encodedValues.stroke || 'none'
}
r={MARK_SIZE / 2}
cx={MARK_SIZE / 2}
cy={MARK_SIZE / 2}
/>
</svg>
) : (
<MarkRenderer item={item} />
)}
{typeof LabelRenderer === 'undefined' ? (
<LegendLabel align="left" margin="0 0 0 4px">
{item.value}
</LegendLabel>
) : (
<LabelRenderer item={item} />
)}
</LegendItem>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Value } from 'vega-lite/build/src/channeldef';
import { ObjectWithKeysFromAndValueType } from '../../encodeable/types/Base';
import { ChannelInput } from '../../encodeable/types/Channel';

export type LegendItemInfo<ChannelTypes> = {
field: string;
value: ChannelInput;
encodedValues: Partial<ObjectWithKeysFromAndValueType<ChannelTypes, Value | undefined>>;
};

export type LegendItemMarkRendererType<ChannelTypes> = React.ComponentType<{
item: LegendItemInfo<ChannelTypes>;
}>;

export type LegendItemLabelRendererType<ChannelTypes> = React.ComponentType<{
item: LegendItemInfo<ChannelTypes>;
}>;

export type LegendItemRendererProps<ChannelTypes> = {
item: LegendItemInfo<ChannelTypes>;
MarkRenderer?: LegendItemMarkRendererType<ChannelTypes>;
LabelRenderer?: LegendItemLabelRendererType<ChannelTypes>;
};

export type LegendItemRendererType<ChannelTypes> = React.ComponentType<
LegendItemRendererProps<ChannelTypes>
>;

export type LegendGroupRendererProps<ChannelTypes> = {
items: LegendItemInfo<ChannelTypes>[];
ItemRenderer?: LegendItemRendererType<ChannelTypes>;
ItemMarkRenderer?: LegendItemMarkRendererType<ChannelTypes>;
ItemLabelRenderer?: LegendItemLabelRendererType<ChannelTypes>;
};

export type LegendGroupRendererType<ChannelTypes> = React.ComponentType<
LegendGroupRendererProps<ChannelTypes>
>;
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { Value } from 'vega-lite/build/src/channeldef';
import { ObjectWithKeysFromAndValueType } from './types/Base';
import { ChannelOptions, EncodingFromChannelsAndOutputs, ChannelType } from './types/Channel';
import {
ChannelOptions,
EncodingFromChannelsAndOutputs,
ChannelType,
ChannelInput,
} from './types/Channel';
import { FullSpec, BaseOptions, PartialSpec } from './types/Specification';
import { isFieldDef } from './types/ChannelDef';
import { isFieldDef, isTypedFieldDef } from './types/ChannelDef';
import ChannelEncoder from './ChannelEncoder';
import { Dataset } from './types/Data';

export default abstract class AbstractEncoder<
// The first 3 generics depends on each other
Expand Down Expand Up @@ -116,6 +122,43 @@ export default abstract class AbstractEncoder<
return Array.from(new Set(fields));
}

getLegendInfos(data: Dataset) {
return Object.keys(this.legends)
.map((field: string) => {
const channelNames = this.legends[field];
const channelEncoder = this.channels[channelNames[0]];

if (isTypedFieldDef(channelEncoder.definition)) {
// Only work for nominal channels now
// TODO: Add support for numerical scale
if (channelEncoder.definition.type === 'nominal') {
const domain = Array.from(new Set(data.map(channelEncoder.get)));

return domain.map((value: ChannelInput) => ({
field,
value,
// eslint-disable-next-line sort-keys
encodedValues: channelNames.reduce(
(
prev: Partial<ObjectWithKeysFromAndValueType<ChannelTypes, Value | undefined>>,
curr,
) => {
const map = prev;
map[curr] = this.channels[curr].encodeValue(value);

return map;
},
{},
),
}));
}
}

return [];
})
.filter(items => items.length > 0);
}

hasLegend() {
return Object.keys(this.legends).length > 0;
}
Expand Down

0 comments on commit 034eda1

Please sign in to comment.