Skip to content
This repository has been archived by the owner on Feb 19, 2022. It is now read-only.

Legend #189

Merged
merged 23 commits into from
Jan 19, 2017
Merged
Show file tree
Hide file tree
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 Aug 24, 2016
e203fe0
updated victory-legend
janesh-travolta Aug 31, 2016
08ecee3
merged master
janesh-travolta Sep 12, 2016
358af4a
minor improvements
janesh-travolta Sep 13, 2016
7d15430
Merge pull request #116 from janesh-travolta/victory-legend
boygirl Sep 21, 2016
e3a9c31
Merge branch 'master' into legend
angelanicholas Jan 11, 2017
8846970
lint and spelling
angelanicholas Jan 18, 2017
6da5333
address PR comments and other refactoring
angelanicholas Jan 18, 2017
619fc26
update demo
angelanicholas Jan 18, 2017
62d081f
update tests
angelanicholas Jan 18, 2017
1294b67
reorder component methods
angelanicholas Jan 18, 2017
4a42957
move leftOffset calc to getLegendState
angelanicholas Jan 18, 2017
080c387
don't pass props around unnecessarily
angelanicholas Jan 18, 2017
1dadd57
fix standalone demo
angelanicholas Jan 18, 2017
d3a59df
move demo for consistency
angelanicholas Jan 18, 2017
1373951
remove state in favor of calculated props in render
angelanicholas Jan 19, 2017
88b1abf
consolidate orientation checks by storing in calculated props
angelanicholas Jan 19, 2017
e9905c8
add theme prop and move default styles
angelanicholas Jan 19, 2017
783031a
fix tests
angelanicholas Jan 19, 2017
d74b1ff
add colorScale prop
angelanicholas Jan 19, 2017
bb1252d
alphabetize props
angelanicholas Jan 19, 2017
ea7a65e
alphabetize default props, remove default padding prop
angelanicholas Jan 19, 2017
bb3a74a
allow height width padding to be specified in theme
angelanicholas Jan 19, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions demo/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";
import ReactDOM from "react-dom";
import AnimationDemo from "./victory-animation-demo";
import LabelDemo from "./victory-label-demo";
import LegendDemo from "./victory-legend-demo";
import TooltipDemo from "./victory-tooltip-demo";
import { Router, Route, Link, hashHistory } from "react-router";

Expand All @@ -20,6 +21,7 @@ const App = React.createClass({
<ul>
<li><Link to="/animation">Victory Animation Demo</Link></li>
<li><Link to="/label">Victory Label Demo</Link></li>
<li><Link to="/legend">Victory Legend</Link></li>
<li><Link to="/tooltip">Victory Tooltip Demo</Link></li>
</ul>
{this.props.children}
Expand All @@ -33,6 +35,7 @@ ReactDOM.render((
<Route path="/" component={App}>
<Route path="animation" component={AnimationDemo}/>
<Route path="label" component={LabelDemo}/>
<Route path="legend" component={LegendDemo}/>
<Route path="tooltip" component={TooltipDemo}/>
</Route>
</Router>
Expand Down
53 changes: 53 additions & 0 deletions demo/victory-legend-demo.js
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;
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { default as VictoryTransition } from "./victory-transition/victory-trans
export { default as VictorySharedEvents } from "./victory-shared-events/victory-shared-events";
export { default as VictoryClipContainer } from "./victory-clip-container/victory-clip-container";
export { default as VictoryTheme } from "./victory-theme/victory-theme";
export { default as VictoryLegend } from "./victory-legend/victory-legend";
export { default as VictoryTooltip } from "./victory-tooltip/victory-tooltip";
export { default as VictoryPortal } from "./victory-portal/victory-portal";
export { default as Portal } from "./victory-portal/portal";
Expand Down
257 changes: 257 additions & 0 deletions src/victory-legend/victory-legend.js
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 });
Copy link
Contributor

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.

Copy link
Contributor Author

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

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;
}
}
8 changes: 8 additions & 0 deletions src/victory-theme/grayscale.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,13 @@ export default {
},
labels: centeredLabelStyles
}
}, baseProps),
legend: assign({
style: {
symbol: {
type: "circle"
},
labels: baseLabelStyles
}
}, baseProps)
};
8 changes: 8 additions & 0 deletions src/victory-theme/material.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,13 @@ export default {
},
labels: centeredLabelStyles
}
}, baseProps),
legend: assign({
style: {
symbol: {
type: "circle"
},
labels: baseLabelStyles
}
}, baseProps)
};
Loading