-
Notifications
You must be signed in to change notification settings - Fork 0
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
Add <Legend> into <LineChart> #14
Conversation
6f52591
to
c9502bb
Compare
@@ -108,12 +110,75 @@ const lineData = [ | |||
y={{ type: 'quantitative', field: 'y' }} | |||
color={{ | |||
field: "size", | |||
type: "quantitative", | |||
type: "ordinal", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's quantitative
the label in legend will be independent to data point value (size
) and paint a discrete scale. This should be ordinal
so the legend will exactly match size
.
docs/charts/LineChart.mdx
Outdated
</div> | ||
</Playground> | ||
|
||
### Multiple lines Without legend |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the screenshot in PR description :-)
docs/charts/LineChart.mdx
Outdated
}} | ||
/> | ||
</div> | ||
</Playground> | ||
|
||
### Multiple lines with custom legend |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the screenshot in PR description :-)
const dimension = useContainerDimension(chartRef); | ||
const { width: legendWidth } = useContainerDimension(legendRef); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The legend should be "outside" of the graph. So we have to have a ref on the <LegendGroup>
, detect its width and bring it into getInnerGraphDimension
to make sure graph will not intersect with legend.
encoding: color, | ||
colors: theme.colors, | ||
}) | ||
: null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make it return null
because we can use non null operator in https://github.com/iCHEF/transcharts/pull/14/files#diff-d2fea036f66e094259cb8577ae881e96R258 . Not sure if there's better way, though.
@@ -99,7 +112,7 @@ export interface Theme { | |||
/** colors used for nominal data */ | |||
category: ReadonlyArray<string>; | |||
sequential: { | |||
scheme: ReadonlyArray<string>; | |||
scheme: ReadonlyArray<ReadonlyArray<string>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adjustment is because scaleOrdinal
(we used in getColorScale
) will map field value to color one by one from the start of scheme array, which will make mapped color too light when domain size is much smaller than the scheme size. For example when we only have large
, small
2 sizes but have 9 colors in scheme, the scaleOrdinal
will map this 2 sizes to the 0th and 1st color in the color scheme, which is too light to read it clearly.
To fix this, the scheme should be an array of array of color (which is https://github.com/d3/d3-scale-chromatic#schemeBlues). When configure color scale, just use the color scheme as large as the domain size, so the scale could map color properly.
}; | ||
}, | ||
[], | ||
[containerRef], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The containerRef
would be null
when attaching ref
on legend and be updated after first render. We have to add containerRef
to detect the change.
() => { | ||
// connect the resize observer on mounted | ||
resizeObsrRef.current = new resizeObserverPolyfill(debouncedResize); | ||
resizeObsrRef.current.observe(containerRef.current!); | ||
if (containerRef.current) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the containerRef.current
could be null when detecting <LegendGroup>
width, we have to check it here.
@@ -64,18 +64,22 @@ export function useContainerDimension( | |||
[], | |||
); | |||
|
|||
useEffect( | |||
useLayoutEffect( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not that sure. But it seems that we should "wait" for the layout change (element attached) and bind the observer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate on why you're choosing useLayoutEffect
over useEffect
?
I believe the later is preferred by the official docs of React Hooks.
Ref: https://reactjs.org/docs/hooks-reference.html#uselayouteffect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some thinking I agree that this should be useEffect
. What I want is I'd like to make sure when the effect is running the container element has been in DOM. But since the containerRef would been changed from null
to the html element before the effect running, useEffect
should be better one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think after you remove the following statements in you could use useEffect
without any issue:
if (graphWidth <= 0 || graphHeight <= 0) {
return null;
}
collisionComponents, | ||
throttleTime = 180, | ||
}) => { | ||
export const HoverLayer = (props: HoverLayerProps) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The point of this fix is, even we have default argument (handleHover
and throttleTime
) in HoverLayer
, we still get typescript error when rendering it without passing them:
/* oops */
<HoverLayer
setHoveredPosAndIndex={...}
clearHovering={...}
collisionPoints={...}
/>
It's because FunctionComponent<HoverLayerProps>
type won't consider the default arguments and regard the handleHover
and throttleTime
are required. So I fixed them with the indication in react-typescript-cheatsheet.
(This won't make docz break so I didn't find out this until I try to build it today 😢 😭 💣 . I think we should fix the difference between docz build
and lerna run prepublish
and add CI check as soon as possible.)
const SCALE_TYPE_TO_LEGEND = { | ||
ordinal: LegendOrdinal, | ||
sequential: LegendLinear, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently we only accept two type of scale for legend. I think it's fine and we can have more if needed.
<div> | ||
<div>{title}</div> | ||
<LegendComponent scale={scale}> | ||
{render} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We pass the render
as children render prop in vx Legend
. See the vx legend gallery and vx legend doc
* LegendGroup will return corresponding legend. | ||
* | ||
*/ | ||
export const LegendGroup = forwardRef((props: LegendGroupProps, ref: RefObject<HTMLDivElement>) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason to have LegendGroup
is because
- We may have multiple legends in one place if there are multiple scales (
color
/shape
/size
...) - I'd like to handle encoding / legend config outside our own
Legend
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second reason is fair enough, but I have some hard time imaging the usage of first reason.
Can you picture a rough example of how a <LegendGroup>
can benefit multiple legends?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -15,7 +15,7 @@ export const themes = { | |||
colors: { | |||
category: schemeCategory10, | |||
sequential: { | |||
scheme: schemeBlues[9], | |||
scheme: schemeBlues, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -42,13 +42,14 @@ const getColorScaleSetting = ({ | |||
}; | |||
} | |||
case 'ordinal': { | |||
const domain = map(data, field).sort((a, b) => Number(a) - Number(b)); | |||
const scale = scaleOrdinal(colors.sequential.scheme).domain(domain); | |||
const domain = sortedUniq(map(data, field).sort((a, b) => Number(a) - Number(b))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have to make sure the element is unique or the scaleOrdinal
will treat same element in domain as different value and map it incorrectly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice change 👍. I think you could put this note into the comment.
6205b24
to
757fd90
Compare
docs/charts/LineChart.mdx
Outdated
data={multiLinesData} | ||
x={{ type: 'quantitative', field: 'x' }} | ||
y={{ type: 'quantitative', field: 'y' }} | ||
color={{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a user, I think the color
prop has became too complex here.
It obviously contains much more than its literal name color
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm... I agree that it's becoming larger. And that's because I tried to use the same (not fully equal to) schema as data encoding in vega-lite. With this we can have (mostly) same properties for different visual feature (color / opacity / size...) to make the api more consistent. And the legend
is also included in encoding object.
Also See #5 for some information about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that leaving vx-legend
to handle the custom legend is a nice design here. Legends are shown by default if the color scale is provided and users are not required to customize the legends.
We may adopt similar approaches to let users customize the tooltip so that our API could be more consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For users familiar with vx
it might be great as they can share existing experiences.
I was thinking it could be even better if we can design a more easy-to-use API, especially we are creating a new library.
But we can talk about that later, there's no hurry to block this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not that sure in the legend
props of color
(That would be another problem that if legend
should be under color
) if we should
- have the same props (except
hide
andrender
) as vx-legend (even just pass the remaining properties oflegend
down to<Legend>
of vx-legend) - have the same props as vega-lite legend config.
Personally I may prefer to (1) because it's easier to implement and also intuitive to vx user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great integration with the vx-legend
library and elegant implementation of the <LegendGroup>
✨
I think properties like direction
if the legend and it's orient
could be furtherly added since charts may not share the same placement of the legend when the layout is different.
It would be nice if a separate document about the usages of <Legend>
in docz
be included to let users know about how to customize the legends 😃 .
packages/graph/src/legend/Legend.tsx
Outdated
scaleType: string; | ||
scale: (val: any) => string; | ||
title: string; | ||
render?: FunctionComponent | null | undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the type of render
be: (labels: ReadonlyArray<object>) => React.ReactNode
?
* LegendGroup will return corresponding legend. | ||
* | ||
*/ | ||
export const LegendGroup = forwardRef((props: LegendGroupProps, ref: RefObject<HTMLDivElement>) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you may destruct the props
here
export const LegendGroup = forwardRef((props: LegendGroupProps, ref: RefObject<HTMLDivElement>) => { | |
export const LegendGroup = forwardRef(({ color }: LegendGroupProps, ref: RefObject<HTMLDivElement>) => { |
const { color } = props; | ||
if (!color || (color.legend && color.legend.hide)) { | ||
return null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice design decision that takes multiple scales into account 👍 .
Maybe TODO: ...
comments about supporting other scales could be added here.
@@ -42,13 +42,14 @@ const getColorScaleSetting = ({ | |||
}; | |||
} | |||
case 'ordinal': { | |||
const domain = map(data, field).sort((a, b) => Number(a) - Number(b)); | |||
const scale = scaleOrdinal(colors.sequential.scheme).domain(domain); | |||
const domain = sortedUniq(map(data, field).sort((a, b) => Number(a) - Number(b))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice change 👍. I think you could put this note into the comment.
docs/charts/LineChart.mdx
Outdated
data={multiLinesData} | ||
x={{ type: 'quantitative', field: 'x' }} | ||
y={{ type: 'quantitative', field: 'y' }} | ||
color={{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that leaving vx-legend
to handle the custom legend is a nice design here. Legends are shown by default if the color scale is provided and users are not required to customize the legends.
We may adopt similar approaches to let users customize the tooltip so that our API could be more consistent.
import { Legend } from './Legend'; | ||
|
||
interface LegendGroupProps { | ||
color?: ColorScale & { legend?: LegendConfig }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some props for configuring the placement like the direction
and orient
(see Legend of vx-lite) could be further included, since these properties are supported in vx-legend
and users may want to apply different settings of direction
and orient
in different charts.
@@ -64,18 +64,22 @@ export function useContainerDimension( | |||
[], | |||
); | |||
|
|||
useEffect( | |||
useLayoutEffect( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think after you remove the following statements in you could use useEffect
without any issue:
if (graphWidth <= 0 || graphHeight <= 0) {
return null;
}
bd37a3c
to
2f95e78
Compare
@garfieldduck @zhusee2 I just added |
2f95e78
to
f31a666
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent implementations of legends and nice documentation. 💯🚀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM as a batch
default: { | ||
break; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(optional comment)
I believe it's possible to save some duplicated codes by making your switch
cases fall-through, and maybe also by checking outer values outside the switch
block:
let graphWidth = (outerWidth <= 0) ? DEFAULT_LENGTH : undefined;
let graphHeight = (outerHeight <= 0) ? DEFAULT_LENGTH : undefined;
switch (orient) {
case LEFT:
case RIGHT:
graphWidth = graphWidth || value;
graphHeight = graphHeight || value;
break;
case TOP:
case BOTTOM:
// also do the calc
break;
}
f31a666
to
717ba5f
Compare
Purpose
Add
<Legend>
component, and integrate it into<LineChart>
.Change
graph
<Legend>
and<LegendGroup>
.colors.sequential.scheme
beReadonlyArray<string>[]
to include multiple color arrays to fix ordinal color scale not map color properly.getInnerGraphDimension
so graph width won't include legend width.<HoverLayer>
without passing defaultProps (throttleTime
,handleHover
).chart
<LegendGroup>
and related encoding props into<LineChart>
.UI screenshot
nominal color field line chart
ordinal color field line chart
line chart without legend
line chart with custom legend
orient=left
orient=top
orient=bottom
direction=horizontal
direction=vertical
Risks
None.
Todos
Consider legend width/height in.getInnerGraphDimension
Add basic legend encoding to allow customize legend.