Skip to content

feat(xy_charts): render legend inside the chart#1031

Merged
markov00 merged 16 commits intoelastic:masterfrom
markov00:2021_01_21-legend_inside_chart
Mar 24, 2021
Merged

feat(xy_charts): render legend inside the chart#1031
markov00 merged 16 commits intoelastic:masterfrom
markov00:2021_01_21-legend_inside_chart

Conversation

@markov00
Copy link
Copy Markdown
Collaborator

@markov00 markov00 commented Feb 17, 2021

Summary

This PR adds the ability to display the legend inside the XY cartesian charts.
The <Settings> property legendPosition will take now an extended version of the position that allow an increased customizability:

... // on SettingsSpec
legendPosition: Position | LegendPositionConfig;

...

type LegendPositionConfig = {
  /**
   * The vertical alignment of the legend
   */
  vAlign: typeof VerticalAlignment.Top | typeof VerticalAlignment.Bottom; // TODO typeof VerticalAlignment.Middle
  /**
   * The horizontal alignment of the legend
   */
  hAlign: typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Right; // TODO typeof HorizontalAlignment.Center
  /**
   * The direction of the legend items.
   * `horizontal` shows all the items listed one a side the other horizontally, wrapping to new lines.
   * `vertical` shows the items in a vertical list
   */
  direction: LayoutDirection;
  /**
   * Remove the legend from the outside chart area, making it floating above the chart.
   * @default false
   */
  floating: boolean;

};

The legend outside the chart only works with the following cases:

direction: vertical, vAlign: top, hAlign: left/right
direction: horizontal, hAlign:left, vAlign: top/bottom

This in the future can be expanded to consider:

  • centred alignment positions like VerticalAlignment.Middle HorizontalAlignment.Center
  • add more customization to the legend, like the number of columns
  • add a grow factor like the flex box layout: shrink, grow, fill can be used to specify how to display the legend items

Screenshot 2021-02-17 at 16 55 39

fix #861

@markov00 markov00 requested a review from elizabetdev February 17, 2021 16:12
@markov00
Copy link
Copy Markdown
Collaborator Author

markov00 commented Feb 17, 2021

@miukimiu can you please double check from the UX prospective if we need to add a background layer below the legend and/or change the current highlight background on each legend item on hover, you can play with that on storybook at: http://localhost:9001/?path=/story/legend--inside-chart thanks

@codecov-io
Copy link
Copy Markdown

codecov-io commented Feb 17, 2021

Codecov Report

Merging #1031 (adc100c) into master (7e2a5e1) will increase coverage by 0.42%.
The diff coverage is 85.34%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1031      +/-   ##
==========================================
+ Coverage   71.55%   71.98%   +0.42%     
==========================================
  Files         381      399      +18     
  Lines       11914    12271     +357     
  Branches     2588     2646      +58     
==========================================
+ Hits         8525     8833     +308     
- Misses       3358     3399      +41     
- Partials       31       39       +8     
Flag Coverage Δ
unittests 71.55% <85.34%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...es/heatmap/state/selectors/get_grid_full_height.ts 26.47% <0.00%> (ø)
..._types/xy_chart/renderer/canvas/axes/tick_label.ts 25.00% <0.00%> (ø)
src/components/portal/types.ts 100.00% <ø> (ø)
src/specs/settings.tsx 90.32% <ø> (ø)
src/components/legend/position_style.ts 47.82% <47.82%> (ø)
...tmap/state/selectors/get_heatmap_container_size.ts 53.33% <50.00%> (ø)
...chart_types/partition_chart/layout/utils/legend.ts 80.00% <100.00%> (ø)
.../partition_chart/state/selectors/compute_legend.ts 100.00% <100.00%> (ø)
..._chart/state/selectors/compute_chart_dimensions.ts 92.59% <100.00%> (ø)
src/chart_types/xy_chart/utils/axis_utils.ts 94.91% <100.00%> (ø)
... and 28 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7e2a5e1...adc100c. Read the comment docs.

@markov00 markov00 marked this pull request as draft February 17, 2021 19:54
@markov00
Copy link
Copy Markdown
Collaborator Author

@monfera @rshen91 @nickofthyme I'd like your opinion on the API as written on the PR description

@markov00 markov00 force-pushed the 2021_01_21-legend_inside_chart branch from f857055 to 21fa501 Compare February 18, 2021 11:19
Copy link
Copy Markdown
Contributor

@elizabetdev elizabetdev left a comment

Choose a reason for hiding this comment

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

I've been experimenting on https://github.com/miukimiu/elastic-charts/tree/2021_01_21-legend_inside_chart how the legend would look like without the light gray background.

On hover, we could make the font-weight bolder and the color darker. But with a bolder font-weight, the text would sometimes shift a little bit. To avoid that we could use a text-shadow. But after experimenting with this solution, I came to the conclusion that:

  • It's hard to notice the hover state in dark themes

So I think the light gray background works better for most scenarios.

legend-hover@2x

Conclusion, LGTM! 🎉

@rshen91
Copy link
Copy Markdown
Contributor

rshen91 commented Feb 19, 2021

@monfera @rshen91 @nickofthyme I'd like your opinion on the API as written on the PR description

I think it might make sense to have an internalLegendPosition property in addition to the legendPosition property to avoid confusion.

Copy link
Copy Markdown
Collaborator

@nickofthyme nickofthyme left a comment

Choose a reason for hiding this comment

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

Sorry this took so long to get to.

I think most of this api is fine but I really don't like the tuple approach. I listed the negatives below:

  • IMHO I think it is more complicated for the chart consumer to interoperate how to position the legend
  • None of the positions mention the legend being internal, just shows positions (e.g. ['top', 'left'])
  • It creates more checking internally to determine if the position is internal and handling the legendPosition as a tuple or a Position type
  • top/bottom must come first then left/right

I think the best approach is to add these to the Position enum or likely another LegendPosition to avoid collisions.

const LegendPosition = Object.freeze({
  ...Position,
  InsideTopLeft: 'ITL',
  InsideTopRight: 'ITR',
  InsideBottomLeft: 'IBL',
  InsideBottomRight: 'IBR',
});

simple API, explicit internal designation, cleaner internal values checks.

@monfera
Copy link
Copy Markdown
Contributor

monfera commented Mar 4, 2021

Is it a full understanding of the distinct envisioned use cases?

Name Dimension Exists? Shape Chart landmark Legend landmark
Top Y yes horizontally spans
vertically minimizes
X:left
Y: top
X:left
Y: bottom
Bottom Y yes horizontally spans
vertically minimizes
X:left
Y: bottom
X:left
Y: top
Left X yes vertically spans
horizontally minimizes
X:left
Y: top
X:right
Y: top
Right X yes horizontally spans
vertically minimizes
X:right
Y: top
X:left
Y: top
InsideTopLeft XY no vertically spans
horizontally minimizes
X:left
Y: top
X:left
Y: top
InsideTopRight XY no vertically spans
horizontally minimizes
X:right
Y: top
X:right
Y: top
InsideBottomLeft XY no vertically spans
horizontally minimizes
X:left
Y: bottom
X:left
Y: bottom
InsideBottomRight XY no vertically spans
horizontally minimizes
X:right
Y: bottom
X:right
Y: bottom

A single enum has the benefit of one, primitive type for the user (no union type), at the slight cost of bundling independent properties (into not too many, easy to understand, most frequently used options).

Where it runs into limit is if we want to add pretty much anything; eg.

  • bring in middle/center for every option as an alternative to left/right/top/bottom - leads to much more than the advised 5-9 options
  • add the possibility of relative or absolute coordinates
  • multiple legends
  • separating the legends from the charts (because then we can have multiple charts on a dashboard that share common legends; also, non-elastic-charts visuals eg. cell-colored tables or maps can use elastic-charts legends)

Addressing the last two are larger exercises that needs more planning anyway.

So either way looks good if the table is a correct summary of the ~1year outlook, maybe enums are simpler (more than 7-9 enums and they become unmanageable) and involve a smaller API change; we could rethink for all these other use cases in a next round. But if we need relative coordinates now, or middle/center, maybe it's a good time to go for the more ambitious undertaking more API design.

@markov00 markov00 marked this pull request as ready for review March 22, 2021 11:46
@monfera
Copy link
Copy Markdown
Contributor

monfera commented Mar 23, 2021

Some specifications don't combine, eg.

  • outside chart + bottom
  • outside chart + left

Maybe it's expected though, left for a future PR

image

const totalVerticalPadding = padding * 2;
let legendHeight = 0;
if (showLegend && isHorizontalLegend(legendPosition)) {
if (showLegend && isHorizontalLegend(Array.isArray(legendPosition) ? legendPosition[1] : legendPosition)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If legendPosition can be an array, where is it specified? On a cursory search I only see

legendPosition: Position | LegendPositionConfig

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Oh thanks, good catch, it was the previous implementation, Let me fix this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Weird that TS doesn't complain here that it narrowed to never and the test will be always falsey; structural typing without structural support 😄

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done in 95d59c9

item: LegendItem;
totalItems: number;
position: Position;
legendPositionConfig: LegendPositionConfig;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As we are already inside LegendItemProps, and the type is prefixed with Legend, maybe the name could shorten to PositionConfig; the other props aren't prefixed with legend either

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done in 95d59c9

return position;
}
return LEGEND_TO_FULL_CONFIG[position];
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Or just

return typeof position === 'object' ? position : LEGEND_TO_FULL_CONFIG[position];

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed in 95d59c9 (not sure why this doesn't shows up here as outdated)

export function getLegendStyle(position: Position, size: BBox, margin: number): LegendStyle {
if (position === Position.Left || position === Position.Right) {
export function getLegendStyle(direction: LegendPositionConfig['direction'], size: BBox, margin: number): LegendStyle {
if (direction === 'vertical') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: here and elsewhere, using the "enum" feels a bit more reified:

if (direction === LayoutDirection.Vertical)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

thanks I'm gonna change every missing enums

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed 95d59c9

return {
flatLegend,
legendAction,
legendColorPicker,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This extraction of the listed legend props here wouldn't be needed if the legends spec props belonged in its cohesive object, via a single composite prop in SettingsSpec. As is, every future prop change/addition needs maintenance here too, so a bit less DRY

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I know, hope to get to this soon

textOffsetY: number;
align: Extract<HorizontalAlignment, 'left' | 'center' | 'right'>;
verticalAlign: Extract<VerticalAlignment, 'top' | 'middle' | 'bottom'>;
align: Extract<
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it intentionally just align rather than horizontalAlign?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've changed only the type declaration here, I can change the propname to reflect it better

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed 95d59c9

@monfera monfera self-requested a review March 23, 2021 16:27
Copy link
Copy Markdown
Contributor

@monfera monfera left a comment

Choose a reason for hiding this comment

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

Works well for me for the inside placed Cartesians. Thanks for assessing the individual comments. Unless there's concern about some of the options not yet functional (see comment with screenshot) it's good to go, it adds useful functionality and doesn't remove existing capabilities

@markov00 markov00 merged commit ba88122 into elastic:master Mar 24, 2021
@markov00 markov00 deleted the 2021_01_21-legend_inside_chart branch March 24, 2021 16:29
Copy link
Copy Markdown
Collaborator

@nickofthyme nickofthyme left a comment

Choose a reason for hiding this comment

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

lol was just about to approve! 🟢

Verified with rotations on chrome. One thing we might want to add in the future is a theme item to set the box-shadow of the text and color to avoid hidden dots.

Image 2021-03-24 at 11 29 33 AM

github-actions bot pushed a commit that referenced this pull request Mar 26, 2021
# [26.1.0](v26.0.0...v26.1.0) (2021-03-26)

### Features

* **a11y:** add basic aria-label to canvas element ([#1084](#1084)) ([1a5aef7](1a5aef7))
* **xy_charts:** render legend inside the chart ([#1031](#1031)) ([ba88122](ba88122)), closes [#861](#861)
@nickofthyme
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 26.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@nickofthyme nickofthyme added the released Issue released publicly label Mar 26, 2021
AMoo-Miki pushed a commit to AMoo-Miki/OpenSearch-Dashboards that referenced this pull request Feb 10, 2022
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 85.34483% with 17 lines in your changes missing coverage. Please review.

Project coverage is 71.98%. Comparing base (7e2a5e1) to head (adc100c).

Files with missing lines Patch % Lines
src/components/legend/position_style.ts 47.82% 12 Missing ⚠️
...es/heatmap/state/selectors/get_grid_full_height.ts 0.00% 2 Missing ⚠️
...tmap/state/selectors/get_heatmap_container_size.ts 50.00% 2 Missing ⚠️
..._types/xy_chart/renderer/canvas/axes/tick_label.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1031      +/-   ##
==========================================
+ Coverage   71.55%   71.98%   +0.42%     
==========================================
  Files         381      399      +18     
  Lines       11914    12271     +357     
  Branches     2588     2646      +58     
==========================================
+ Hits         8525     8833     +308     
- Misses       3358     3399      +41     
- Partials       31       39       +8     
Flag Coverage Δ
unittests 71.55% <85.34%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released Issue released publicly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Legend positioned inside the charts

7 participants