This repository has been archived by the owner on Feb 19, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 76
Legend #189
Merged
Merged
Legend #189
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
dc3142d
Add initial victory-legend
janesh-travolta e203fe0
updated victory-legend
janesh-travolta 08ecee3
merged master
janesh-travolta 358af4a
minor improvements
janesh-travolta 7d15430
Merge pull request #116 from janesh-travolta/victory-legend
boygirl e3a9c31
Merge branch 'master' into legend
angelanicholas 8846970
lint and spelling
angelanicholas 6da5333
address PR comments and other refactoring
angelanicholas 619fc26
update demo
angelanicholas 62d081f
update tests
angelanicholas 1294b67
reorder component methods
angelanicholas 4a42957
move leftOffset calc to getLegendState
angelanicholas 080c387
don't pass props around unnecessarily
angelanicholas 1dadd57
fix standalone demo
angelanicholas d3a59df
move demo for consistency
angelanicholas 1373951
remove state in favor of calculated props in render
angelanicholas 88b1abf
consolidate orientation checks by storing in calculated props
angelanicholas e9905c8
add theme prop and move default styles
angelanicholas 783031a
fix tests
angelanicholas d74b1ff
add colorScale prop
angelanicholas bb1252d
alphabetize props
angelanicholas ea7a65e
alphabetize default props, remove default padding prop
angelanicholas bb3a74a
allow height width padding to be specified in theme
angelanicholas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import React from "react"; | ||
import { VictoryLegend } from "../src/index"; | ||
|
||
const svgStyle = { border: "1px solid #ccc" }; | ||
const data = [{ | ||
name: "Series 1", | ||
symbol: { | ||
type: "circle", | ||
fill: "green" | ||
} | ||
}, { | ||
name: "Long Series Name", | ||
symbol: { | ||
type: "triangleUp", | ||
fill: "blue" | ||
} | ||
}, { | ||
name: "Series 3", | ||
symbol: { | ||
type: "diamond", | ||
fill: "pink" | ||
} | ||
}, { | ||
name: "Series 4", | ||
symbol: { type: "plus" } | ||
}, { | ||
name: "Series 5", | ||
symbol: { | ||
type: "star", | ||
fill: "red" | ||
} | ||
}]; | ||
|
||
const LegendDemo = () => ( | ||
<div className="demo"> | ||
<VictoryLegend data={data} /> | ||
<svg | ||
height={56} | ||
width={525} | ||
style={svgStyle} | ||
> | ||
<VictoryLegend | ||
data={data} | ||
padding={20} | ||
standalone={false} | ||
orientation="horizontal" | ||
style={{ labels: { fill: "#ccc" }}} | ||
/> | ||
</svg> | ||
</div> | ||
); | ||
|
||
export default LegendDemo; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
import React, { PropTypes } from "react"; | ||
import { PropTypes as CustomPropTypes, Style, TextSize, Helpers } from "../victory-util/index"; | ||
import { merge, isEmpty, defaults, sumBy, maxBy } from "lodash"; | ||
import VictoryLabel from "../victory-label/victory-label"; | ||
import VictoryContainer from "../victory-container/victory-container"; | ||
import VictoryTheme from "../victory-theme/victory-theme"; | ||
import Point from "../victory-primitives/point"; | ||
|
||
const defaultLegendData = [ | ||
{ name: "Series 1" }, | ||
{ name: "Series 2" } | ||
]; | ||
|
||
export default class VictoryLegend extends React.Component { | ||
static displayName = "VictoryLegend"; | ||
|
||
static role = "legend"; | ||
|
||
static propTypes = { | ||
colorScale: PropTypes.oneOfType([ | ||
PropTypes.arrayOf(PropTypes.string), | ||
PropTypes.oneOf([ | ||
"greyscale", "qualitative", "heatmap", "warm", "cool", "red", "green", "blue" | ||
]) | ||
]), | ||
containerComponent: PropTypes.element, | ||
data: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
name: PropTypes.string.isRequired, | ||
label: PropTypes.object, | ||
symbol: PropTypes.object | ||
}) | ||
), | ||
dataComponent: PropTypes.element, | ||
groupComponent: PropTypes.element, | ||
gutter: PropTypes.number, | ||
height: PropTypes.oneOfType([ | ||
CustomPropTypes.nonNegative, | ||
PropTypes.func | ||
]), | ||
labelComponent: PropTypes.element, | ||
orientation: PropTypes.oneOf(["horizontal", "vertical"]), | ||
padding: PropTypes.oneOfType([ | ||
PropTypes.number, | ||
PropTypes.shape({ | ||
top: PropTypes.number, | ||
bottom: PropTypes.number, | ||
left: PropTypes.number, | ||
right: PropTypes.number | ||
}) | ||
]), | ||
standalone: PropTypes.bool, | ||
style: PropTypes.shape({ | ||
symbol: PropTypes.object, | ||
label: PropTypes.object | ||
}), | ||
symbolSpacer: PropTypes.number, | ||
theme: PropTypes.object, | ||
width: PropTypes.oneOfType([ | ||
CustomPropTypes.nonNegative, | ||
PropTypes.func | ||
]), | ||
x: PropTypes.number.isRequired, | ||
y: PropTypes.number.isRequired | ||
}; | ||
|
||
static defaultProps = { | ||
containerComponent: <VictoryContainer/>, | ||
dataComponent: <Point/>, | ||
groupComponent: <g/>, | ||
gutter: 10, | ||
labelComponent: <VictoryLabel/>, | ||
orientation: "vertical", | ||
standalone: true, | ||
style: {}, | ||
symbolSpacer: 8, | ||
theme: VictoryTheme.grayscale, | ||
x: 0, | ||
y: 0 | ||
}; | ||
|
||
calculateLegendHeight(textSizes, padding, isHorizontal) { | ||
const { data, gutter } = this.props; | ||
const contentHeight = isHorizontal | ||
? maxBy(textSizes, "height").height | ||
: sumBy(textSizes, "height") + gutter * (data.length - 1); | ||
|
||
return padding.top + contentHeight + padding.bottom; | ||
} | ||
|
||
calculateLegendWidth(textSizes, padding, isHorizontal) { | ||
const { data, gutter, symbolSpacer } = this.props; | ||
const contentWidth = isHorizontal | ||
? sumBy(textSizes, "width") + (gutter + symbolSpacer * 3) * (data.length - 1) | ||
: maxBy(textSizes, "width").width + symbolSpacer * 2; | ||
|
||
return padding.left + contentWidth + padding.right; | ||
} | ||
|
||
getColorScale(theme) { | ||
const { colorScale } = this.props; | ||
let colorScaleOptions = colorScale || theme.colorScale; | ||
|
||
if (typeof colorScaleOptions === "string") { | ||
colorScaleOptions = Style.getColorScale(colorScaleOptions); | ||
} | ||
|
||
return !isEmpty(theme) ? colorScaleOptions || theme.colorScale : colorScaleOptions || []; | ||
} | ||
|
||
getCalculatedProps() { // eslint-disable-line max-statements | ||
const { role } = this.constructor; | ||
const { data, orientation, theme } = this.props; | ||
let { height, padding, width } = this.props; | ||
const legendTheme = theme && theme[role] ? theme[role] : {}; | ||
const colorScale = this.getColorScale(legendTheme); | ||
const isHorizontal = orientation === "horizontal"; | ||
const symbolStyles = []; | ||
const labelStyles = []; | ||
let leftOffset = 0; | ||
|
||
padding = Helpers.getPadding({ padding: padding || theme.padding }); | ||
height = height || theme.height; | ||
width = width || theme.width; | ||
|
||
const textSizes = data.map((datum, i) => { | ||
const labelStyle = this.getStyles(datum, legendTheme, "labels"); | ||
symbolStyles[i] = this.getStyles(datum, legendTheme, "symbol", colorScale[i]); | ||
labelStyles[i] = labelStyle; | ||
|
||
const textSize = TextSize.approximateTextSize(datum.name, labelStyle); | ||
textSize.leftOffset = leftOffset; | ||
leftOffset += textSize.width; | ||
|
||
return textSize; | ||
}); | ||
|
||
if (!height) { | ||
height = this.calculateLegendHeight(textSizes, padding, isHorizontal); | ||
} | ||
if (!width) { | ||
width = this.calculateLegendWidth(textSizes, padding, isHorizontal); | ||
} | ||
|
||
return Object.assign({}, | ||
this.props, | ||
{ isHorizontal, height, labelStyles, padding, symbolStyles, textSizes, width } | ||
); | ||
} | ||
|
||
getStyles(datum, theme, key, color) { // eslint-disable-line max-params | ||
const colorScale = color ? { fill: color } : {}; | ||
return merge({}, theme.style[key], colorScale, this.props.style[key], datum[key]); | ||
} | ||
|
||
getSymbolSize(datum, fontSize) { | ||
return datum.symbol && datum.symbol.size ? datum.symbol.size : fontSize / 2.5; | ||
} | ||
|
||
getSymbolProps(datum, props, i) { | ||
const { | ||
gutter, labelStyles, isHorizontal, padding, symbolSpacer, symbolStyles, textSizes | ||
} = props; | ||
const { leftOffset } = textSizes[i]; | ||
const { fontSize } = labelStyles[i]; | ||
const symbolShift = fontSize / 2; | ||
const style = symbolStyles[i]; | ||
|
||
const symbolCoords = isHorizontal ? { | ||
x: padding.left + leftOffset + symbolShift + (fontSize + symbolSpacer + gutter) * i, | ||
y: padding.top + symbolShift | ||
} : { | ||
x: padding.left + symbolShift, | ||
y: padding.top + symbolShift + (fontSize + gutter) * i | ||
}; | ||
|
||
return { | ||
key: `symbol-${i}`, | ||
style, | ||
size: this.getSymbolSize(datum, fontSize), | ||
symbol: style.type, | ||
...symbolCoords | ||
}; | ||
} | ||
|
||
getLabelProps(datum, props, i) { | ||
const { gutter, isHorizontal, symbolSpacer, labelStyles, textSizes, padding } = props; | ||
const style = labelStyles[i]; | ||
const { fontSize } = style; | ||
const symbolShift = fontSize / 2; | ||
|
||
const labelCoords = isHorizontal ? { | ||
x: padding.left + textSizes[i].leftOffset + (fontSize + symbolSpacer) * (i + 1) + gutter * i, | ||
y: padding.top + symbolShift | ||
} : { | ||
x: padding.left + fontSize + symbolSpacer, | ||
y: padding.top + symbolShift + (fontSize + gutter) * i | ||
}; | ||
|
||
return { | ||
key: `label-${i}`, | ||
style, | ||
text: datum.name, | ||
...labelCoords | ||
}; | ||
} | ||
|
||
renderLegendItems(props) { | ||
const { data, dataComponent, labelComponent } = props; | ||
const legendData = isEmpty(data) ? defaultLegendData : data; | ||
const length = legendData.length; | ||
const dataComponents = []; | ||
const labelComponents = []; | ||
|
||
for (let i = 0; i < length; i++) { | ||
const datum = legendData[i]; | ||
|
||
dataComponents[i] = React.cloneElement( | ||
dataComponent, | ||
this.getSymbolProps(datum, props, i) | ||
); | ||
labelComponents[i] = React.cloneElement( | ||
labelComponent, | ||
this.getLabelProps(datum, props, i) | ||
); | ||
} | ||
|
||
return [...dataComponents, ...labelComponents]; | ||
} | ||
|
||
renderGroup(props, children) { | ||
const { groupComponent, height, width, standalone, x, y } = props; | ||
const groupProps = { role: "presentation" }; | ||
|
||
if (!standalone) { | ||
Object.assign(groupProps, { height, width, x, y }); | ||
} | ||
|
||
return React.cloneElement(groupComponent, groupProps, children); | ||
} | ||
|
||
renderContainer(props, children) { | ||
const { containerComponent, height, width, x, y, style } = props; | ||
|
||
return React.cloneElement( | ||
containerComponent, | ||
{ x, y, height, width, style: defaults({}, style) }, | ||
children | ||
); | ||
} | ||
|
||
render() { | ||
const props = this.getCalculatedProps(); | ||
const group = this.renderGroup(props, this.renderLegendItems(props)); | ||
return props.standalone ? this.renderContainer(props, group) : group; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
You'll need to remove padding from default props, and make this check respect padding = 0. Also, if you remove padding from default props, this should have a fallback right in the code like props.padding || theme.padding || 0; except with existence checking that respects zero.
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.
Helpers.getPadding has a 0 fallback already if
padding || theme.padding
is undefined, and padding was removed from defaultProps in the previous commit