Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/box plot labels #17579

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
22 changes: 17 additions & 5 deletions src/chart/boxplot/BoxplotView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import ExtensionAPI from '../../core/ExtensionAPI';
import SeriesData from '../../data/SeriesData';
import { BoxplotItemLayout } from './boxplotLayout';
import { saveOldStyle } from '../../animation/basicTransition';
import { ViewRootGroup } from '../../util/types';
import { setBoxLabels } from '../../label/labelStyle';

class BoxplotView extends ChartView {
static type = 'boxplot';
Expand All @@ -52,7 +54,7 @@ class BoxplotView extends ChartView {
.add(function (newIdx) {
if (data.hasValue(newIdx)) {
const itemLayout = data.getItemLayout(newIdx) as BoxplotItemLayout;
const symbolEl = createNormalBox(itemLayout, data, newIdx, constDim, true);
const symbolEl = createNormalBox(itemLayout, data, newIdx, constDim, group, true);
data.setItemGraphicEl(newIdx, symbolEl);
group.add(symbolEl);
}
Expand All @@ -68,11 +70,11 @@ class BoxplotView extends ChartView {

const itemLayout = data.getItemLayout(newIdx) as BoxplotItemLayout;
if (!symbolEl) {
symbolEl = createNormalBox(itemLayout, data, newIdx, constDim);
symbolEl = createNormalBox(itemLayout, data, newIdx, constDim, group);
}
else {
saveOldStyle(symbolEl);
updateNormalBoxData(itemLayout, symbolEl, data, newIdx);
updateNormalBoxData(itemLayout, symbolEl, data, newIdx, group);
}

group.add(symbolEl);
Expand Down Expand Up @@ -144,6 +146,7 @@ function createNormalBox(
data: SeriesData,
dataIndex: number,
constDim: number,
group: ViewRootGroup,
isInit?: boolean
) {
const ends = itemLayout.ends;
Expand All @@ -156,7 +159,7 @@ function createNormalBox(
}
});

updateNormalBoxData(itemLayout, el, data, dataIndex, isInit);
updateNormalBoxData(itemLayout, el, data, dataIndex, group, isInit);

return el;
}
Expand All @@ -166,6 +169,7 @@ function updateNormalBoxData(
el: BoxPath,
data: SeriesData,
dataIndex: number,
group: ViewRootGroup,
isInit?: boolean
) {
const seriesModel = data.hostModel;
Expand All @@ -180,11 +184,19 @@ function updateNormalBoxData(

el.useStyle(data.getItemVisual(dataIndex, 'style'));
el.style.strokeNoScale = true;

el.z2 = 100;

const itemModel = data.getItemModel<BoxplotDataItemOption>(dataIndex);
const emphasisModel = itemModel.getModel('emphasis');
const labelModel = seriesModel.get('label');

if (labelModel.show) {
const formattedLabels =
((seriesModel as BoxplotSeriesModel).getRawValue(dataIndex) as number[])
.splice(1).map((value: number) => labelModel.formatter(value).toString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the correct way to format labels. If formatter is not defined, this should go wrong. You should probably do like sunburst.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ovilia I am not sure if I understood correctly - getFormattedLabel is defined to return a string but somehow returns an object when being called with dataIndex.

I think I am missing something here, could you clarify how you would implement the formatting of the labels?

Thanks


setBoxLabels(itemLayout.ends, formattedLabels, labelModel, group);
}

setStatesStylesFromModel(el, itemModel);

Expand Down
69 changes: 65 additions & 4 deletions src/label/labelStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ import {
ColorString,
ZRStyleProps,
AnimationOptionMixin,
InterpolatableValue
InterpolatableValue,
ViewRootGroup
} from '../util/types';
import GlobalModel from '../model/Global';
import { isFunction, retrieve2, extend, keys, trim } from 'zrender/src/core/util';
import { SPECIAL_STATES, DISPLAY_STATES } from '../util/states';
import { deprecateReplaceLog } from '../util/log';
import { makeInner, interpolateRawValues } from '../util/model';
import SeriesData from '../data/SeriesData';
import { initProps, updateProps } from '../util/graphic';
import * as graphic from '../util/graphic';

type TextCommonParams = {
/**
Expand Down Expand Up @@ -292,6 +293,66 @@ function setLabelStyle<TLabelDataIndex>(
}
export { setLabelStyle };

/**
* Set and create specific labels for box plot.
*
* This method should only be used for this case,
* as it manually creates multiple alternating labels
* specifically for the box plot.
*/
export function setBoxLabels(boxItemPositions: number[][], formattedLabels: Array<string>,
labelOption: any, group: ViewRootGroup) {
// get sorted and unique y positions of box items
const yPositions = boxItemPositions.map((pos: number[]) => pos[1]);
const uniqueYPositions: number[] = [];
yPositions.forEach(position => {
if (!uniqueYPositions.includes(position)) {
uniqueYPositions.push(position);
}
});
uniqueYPositions.sort(function (a: number, b: number) {
return b - a;
});

boxItemPositions.sort(function (a: number[], b: number[]) {
return a[0] - b[0];
});

// get alternating y positions for labels to avoid overlap
const uniqueAlternatingPositions = uniqueYPositions.map((posY: number, ind: number) => {
const matchingPositions = boxItemPositions.filter((orgPos: number[]) => orgPos[1] === posY);
const index = (ind % 2 === 0) ? 0 : matchingPositions.length - 1;
return matchingPositions[index];
});

// create labels and add them to their respective positions
formattedLabels.forEach((labelText: string, ind: number) => {
if (labelOption.show === 'iqr' && (ind === 0 || ind === 4)) {
return;
}
if (labelOption.show === 'median' && (ind !== 2)) {
return;
}
if (labelOption.show === 'whiskers' && (ind !== 0 && ind !== 4)) {
return;
}
const label = new graphic.Text({
style: {
text: labelText
},
z2: 1000
});
const defaultOffset = 5;
const defaultXOffset = (ind % 2) === 0 ? (-(label.getBoundingRect().width + defaultOffset)) : defaultOffset;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said before, we should provide a way to let developers decide where to display the label to the left or right. The label.align can be changed into 'left' | 'right' | 'center' | { iqr: 'left' | 'right' | 'center' , ... }.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ovilia Sorry, maybe I am missing something here as well but there is no single label. We have five labels, right? I still don't get how someone could choose the align for the five in a way that makes sense - e.g. all labels on one side - even though they might overlap?

const defaultYOffset = -defaultOffset;
const customOffset = labelOption.offset ? labelOption.offset : [0, 0];
label.x = uniqueAlternatingPositions[ind][0] + defaultXOffset + customOffset[0],
label.y = uniqueAlternatingPositions[ind][1] + defaultYOffset + customOffset[1],
group.add(label);
});
}


export function getLabelStatesModels<LabelName extends string = 'label'>(
itemModel: Model<StatesOptionMixin<any, any> & Partial<Record<LabelName, any>>>,
labelName?: LabelName
Expand Down Expand Up @@ -726,8 +787,8 @@ export function animateLabelValue(

(textEl as ZRText & {percent?: number}).percent = 0;
(labelInnerStore.prevValue == null
? initProps
: updateProps
? graphic.initProps
: graphic.updateProps
)<TextProps & {percent?: number}>(textEl, {
// percent is used to prevent animation from being aborted #15916
percent: 1
Expand Down
Loading