Skip to content
Snippets Groups Projects
Commit 4d70d96e authored by Tom Robinson's avatar Tom Robinson
Browse files

Initial AddSeriesModal and multiseries Bar chart

parent f5e70aad
No related branches found
No related tags found
No related merge requests found
Showing
with 442 additions and 73 deletions
......@@ -23,7 +23,7 @@ export default ComposedComponent => class extends Component {
}
componentDidUpdate() {
const { width, height } = React.findDOMNode(this).getBoundingClientRect()
const { width, height } = React.findDOMNode(this).getBoundingClientRect();
if (this.state.width !== width || this.state.height !== height) {
this.setState({ width, height });
}
......
......@@ -2,9 +2,12 @@ import React, { Component, PropTypes } from "react";
import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
import cx from "classnames";
export default class LoadingAndErrorWrapper extends Component {
static propTypes = {
className: PropTypes.any,
noBackground: PropTypes.bool,
error: PropTypes.any,
loading: PropTypes.any
};
......@@ -32,14 +35,17 @@ export default class LoadingAndErrorWrapper extends Component {
}
render() {
let contentClassName = cx("wrapper py4 text-brand text-centered flex-full flex flex-column layout-centered", {
"bg-white": !this.props.noBackground
});
return (
<div className={this.props.className}>
{ this.props.error ?
<div className="wrapper py4 text-brand text-centered flex-full bg-white">
<div className={contentClassName}>
<h2 className="text-normal text-grey-2">{this.getErrorMessage()}</h2>
</div>
: this.props.loading ?
<div className="wrapper py4 text-brand text-centered flex-full bg-white">
<div className={contentClassName}>
<LoadingSpinner />
<h2 className="text-normal text-grey-2 mt1">Loading...</h2>
</div>
......
......@@ -14,9 +14,10 @@ export default class Tooltip extends Component {
static defaultProps = {};
render() {
let { tooltipElement } = this.props;
let { tooltipElement, className } = this.props;
return (
<span
className={className}
onMouseEnter={() => this.setState({ isOpen: true })}
onMouseLeave={() => this.setState({ isOpen: false })}
>
......
......@@ -17,6 +17,7 @@
}
.Card-title {
color: #3F3A3A;
font-size: 0.8em;
}
......
......@@ -74,9 +74,8 @@
animation-name: fade-out-yellow;
}
.Dash--editing .DashCard:hover .Card {
border: 1px dashed var(--brand-color);
background-color: #F6FAFD;
.Dash--editing .DashCard:hover .Card .Card-heading {
z-index: 2;
}
.Dash--editing .DashCard:hover .DashCard-actions {
......@@ -148,3 +147,10 @@
.Dash--dragging .DashCard-actions {
pointer-events: none !important;
}
.Modal.AddSeriesModal {
height: 80%;
max-height: 600px;
width: 80%;
max-width: 1024px;
}
import React, { Component, PropTypes } from "react";
import Visualization from "metabase/visualizations/Visualization.jsx";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
import Icon from "metabase/components/Icon.jsx";
import Tooltip from "metabase/components/Tooltip.jsx";
import CheckBox from "metabase/components/CheckBox.jsx";
import _ from "underscore";
import cx from "classnames";
export default class AddSeriesModal extends Component {
constructor(props, context) {
super(props, context);
this.state = {
searchValue: "",
error: null,
series: props.dashcard.series || [],
seriesData: {},
badCards: {}
};
_.bindAll(this, "onSearchChange", "onDone", "filterCards")
}
static propTypes = {};
static defaultProps = {};
async componentDidMount() {
try {
await this.props.fetchCards();
} catch (error) {
this.setState({ error });
}
}
onSearchChange(e) {
this.setState({ searchValue: e.target.value.toLowerCase() });
}
onCardChange(card, e) {
let enabled = e.target.checked;
let series = this.state.series.filter(c => c.id !== card.id);
if (enabled) {
series.push(card);
if (this.state.seriesData[card.id] === undefined) {
this.setState({ state: "loading" });
setTimeout(() => {
let data = {
rows: [
["Doohickey", Math.round(Math.random() * 5000)],
["Gadget", Math.round(Math.random() * 5000)],
["Gizmo", Math.round(Math.random() * 5000)],
["Widget", Math.round(Math.random() * 5000)]
]
}
if (card.dataset_query.type === "query" || Math.random() > 0.75) {
this.setState({
state: null,
seriesData: { ...this.state.seriesData, [card.id]: data }
});
} else {
this.setState({
state: "incompatible",
series: this.state.series.filter(c => c.id !== card.id),
seriesData: { ...this.state.seriesData, [card.id]: false },
badCards: { ...this.state.badCards, [card.id]: true }
});
setTimeout(() => this.setState({ state: null }), 2000);
}
}, 1000);
}
}
this.setState({ series });
}
onDone() {
// this.props.onDone(this.state.series);
}
filterCards(cards) {
let { card } = this.props.dashcard;
let { searchValue } = this.state;
return cards.filter(c => {
if (c.id === card.id) {
return false;
} else if (searchValue && c.name.toLowerCase().indexOf(searchValue) < 0) {
return false;
} else {
return true;
}
});
}
render() {
let { card, dataset } = this.props.dashcard;
let data = (dataset && dataset.data);
let cards = this.props.cards;
let error = this.state.error;
let filteredCards;
if (!error && cards) {
filteredCards = this.filterCards(cards);
if (filteredCards.length === 0) {
error = new Error("Whoops, no compatible questions match your search.");
}
// SQL cards at the bottom
filteredCards.sort((a, b) => {
if (a.dataset_query.type !== "query") {
return 1;
} else if (b.dataset_query.type !== "query") {
return -1;
} else {
return 0;
}
})
}
let badCards = this.state.badCards;
let enabledCards = {};
for (let c of this.state.series) {
enabledCards[c.id] = true;
}
let series = this.state.series.map(c => ({
card: c,
data: this.state.seriesData[c.id]
})).filter(s => !!s.data);
return (
<div className="absolute top left bottom right flex">
<div className="flex flex-column flex-full">
<div className="flex-no-shrink h3 pl4 pt4 pb1 text-bold">Add data</div>
<div className="flex flex-full relative">
<Visualization
className="flex-full"
card={card}
data={data}
series={series}
isDashboard={true}
onAddSeries={this.props.onAddSeries}
/>
{ this.state.state &&
<div className="absolute top left bottom right flex layout-centered" style={{ backgroundColor: "rgba(255,255,255,0.80)" }}>
{ this.state.state === "loading" ?
<div className="h3 rounded bordered p3 bg-white shadowed">Applying Question</div>
: this.state.state === "incompatible" ?
<div className="h3 rounded bordered p3 bg-error border-error text-white">That question isn't compatible</div>
: null }
</div>
}
</div>
<div className="flex-no-shrink pl4 pb4 pt1">
<button className="Button Button--primary" onClick={this.onDone}>Done</button>
<button className="Button Button--borderless" onClick={this.props.onClose}>Cancel</button>
</div>
</div>
<div className="border-left flex flex-column" style={{width: 370, backgroundColor: "#F8FAFA", borderColor: "#DBE1DF" }}>
<div className="flex-no-shrink border-bottom flex flex-row align-center" style={{ borderColor: "#DBE1DF" }}>
<Icon className="ml2" name="search" width={16} height={16} />
<input className="h4 input full pl1" style={{ border: "none", backgroundColor: "transparent" }} type="search" placeholder="Search for a question" onChange={this.onSearchChange}/>
</div>
<LoadingAndErrorWrapper className="flex flex-full" loading={!filteredCards} error={error} noBackground>
{ () =>
<ul className="flex-full scroll-y">
{filteredCards.map(card =>
<li key={card.id} className={cx("my1 px2 py1 flex align-center", { disabled: badCards[card.id] })}>
<span className="px1 flex-no-shrink">
<CheckBox checked={enabledCards[card.id]} onChange={this.onCardChange.bind(this, card)}/>
</span>
<span className="px1">
{card.name}
</span>
{ card.dataset_query.type !== "query" &&
<Tooltip className="px1 flex-align-right" tooltipElement="We're not sure if this question is compatible">
<Icon className="text-grey-2 text-grey-4-hover cursor-pointer" name="warning" width={20} height={20} />
</Tooltip>
}
</li>
)}
</ul>
}
</LoadingAndErrorWrapper>
</div>
</div>
);
}
}
......@@ -3,7 +3,9 @@
import React, { Component, PropTypes } from "react";
import Visualization from "metabase/visualizations/Visualization.jsx";
import visualizations from "metabase/visualizations";
import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
import Icon from "metabase/components/Icon.jsx";
import cx from "classnames";
......@@ -61,7 +63,15 @@ export default class DashCard extends Component {
}
if (card && data) {
return <Visualization className="flex-full" card={card} data={data} isDashboard={true} />;
return (
<Visualization
className="flex-full"
card={card}
data={data}
isDashboard={true}
onAddSeries={this.props.onAddSeries}
/>
);
}
return (
......@@ -74,24 +84,40 @@ export default class DashCard extends Component {
componentDidUpdate() {
let titleElement = React.findDOMNode(this.refs.title);
// have to restore the text in case we previously clamped it :-/
titleElement.textContent = this.props.dashcard.card.name;
$clamp(titleElement, { clamp: 2 });
if (titleElement) {
// have to restore the text in case we previously clamped it :-/
titleElement.textContent = this.props.dashcard.card.name;
$clamp(titleElement, { clamp: 2 });
}
}
render() {
let { card } = this.props.dashcard;
let dc = this.props.dashcard;
let { card } = dc;
let CardVisualization = visualizations.get(card.display);
let recent = this.props.dashcard.isAdded;
return (
<div className={"Card bordered rounded flex flex-column " + cx({ "Card--recent": recent })}>
<div className="Card-heading my1 px2">
<a data-metabase-event={"Dashboard;Card Link;"+card.display} className="Card-title link" href={"/card/"+card.id+"?clone"}>
<div ref="title" className="h3 text-normal my1">
{card.name}
<div>
<div className={"Card bordered rounded flex flex-column " + cx({ "Card--recent": recent })}>
{ !CardVisualization.noHeader &&
<div className="Card-heading my1 px2">
<a data-metabase-event={"Dashboard;Card Link;"+card.display} className="Card-title no-decoration" href={"/card/"+card.id+"?clone"}>
<div ref="title" className="h3 text-bold my1">
{card.name}
</div>
</a>
</div>
}
{this.renderCard()}
</div>
<div className="DashCard-actions absolute top right text-brand p2">
<a href="#" onClick={this.props.onEdit}>
<Icon className="my1 mr1" name="pencil" width="18" height="18" />
</a>
<a data-metabase-event="Dashboard;Remove Card Modal" href="#" onClick={this.props.onRemove}>
<Icon className="my1 mr1" name="trash" width="18" height="18" />
</a>
</div>
{this.renderCard()}
</div>
);
}
......
......@@ -3,10 +3,10 @@ import React, { Component, PropTypes } from "react";
import { Responsive as ResponsiveReactGridLayout } from "react-grid-layout";
import MetabaseAnalytics from "metabase/lib/analytics";
import Icon from "metabase/components/Icon.jsx";
import DashCard from "./DashCard.jsx";
import Modal from "metabase/components/Modal.jsx";
import RemoveFromDashboardModal from "./RemoveFromDashboardModal.jsx";
import AddSeriesModal from "./AddSeriesModal.jsx";
import _ from "underscore";
import cx from "classnames";
......@@ -18,11 +18,12 @@ export default class DashboardGrid extends Component {
this.state = {
layouts: this.getLayouts(props),
removeModalDashCard: null,
addSeriesModalDashCard: null,
rowHeight: 0,
isDragging: false
};
_.bindAll(this, "calculateSizing");
_.bindAll(this, "calculateSizing", "onDashCardMouseDown");
}
static propTypes = {
......@@ -85,26 +86,34 @@ export default class DashboardGrid extends Component {
return { lg: mainLayout, sm: mobileLayout };
}
onEditDashCard(dc) {
// if editing and card is dirty prompt to save changes
if (this.props.isEditing && this.props.isDirty) {
if (!confirm("You have unsaved changes to this dashboard, are you sure you want to discard them?")) {
return;
}
}
this.props.onChangeLocation("/card/" + dc.card_id + "?from=" + encodeURIComponent("/dash/" + dc.dashboard_id));
}
renderRemoveModal() {
// can't use PopoverWithTrigger due to strange interaction with ReactGridLayout
return this.state.removeModalDashCard != null && (
<Modal>
<RemoveFromDashboardModal
let isOpen = this.state.removeModalDashCard != null;
return (
<Modal isOpen={isOpen}>
{ isOpen && <RemoveFromDashboardModal
dashcard={this.state.removeModalDashCard}
dashboard={this.props.dashboard}
removeCardFromDashboard={this.props.removeCardFromDashboard}
onClose={() => this.setState({ removeModalDashCard: null })}
/>
/> }
</Modal>
);
}
renderAddSeriesModal() {
// can't use PopoverWithTrigger due to strange interaction with ReactGridLayout
let isOpen = this.state.addSeriesModalDashCard != null;
return (
<Modal className="Modal AddSeriesModal" isOpen={isOpen}>
{ isOpen && <AddSeriesModal
dashcard={this.state.addSeriesModalDashCard}
dashboard={this.props.dashboard}
cards={this.props.cards}
fetchCards={this.props.fetchCards}
removeCardFromDashboard={this.props.removeCardFromDashboard}
onClose={() => this.setState({ addSeriesModalDashCard: null })}
/> }
</Modal>
);
}
......@@ -148,6 +157,24 @@ export default class DashboardGrid extends Component {
}
}
onDashCardEdit(dc) {
// if editing and card is dirty prompt to save changes
if (this.props.isEditing && this.props.isDirty) {
if (!confirm("You have unsaved changes to this dashboard, are you sure you want to discard them?")) {
return;
}
}
this.props.onChangeLocation("/card/" + dc.card_id + "?from=" + encodeURIComponent("/dash/" + dc.dashboard_id));
}
onDashCardRemove(dc) {
this.setState({ removeModalDashCard: dc });
}
onDashCardAddSeries(dc) {
this.setState({ addSeriesModalDashCard: dc });
}
render() {
var { dashboard } = this.props;
return (
......@@ -172,25 +199,20 @@ export default class DashboardGrid extends Component {
onDragStop={(...args) => this.onDragStop(...args)}
>
{dashboard.ordered_cards.map(dc =>
<div key={dc.id} className="DashCard" onMouseDownCapture={(...args) => this.onDashCardMouseDown(...args)}>
<div key={dc.id} className="DashCard" onMouseDownCapture={this.onDashCardMouseDown}>
<DashCard
key={dc.id}
dashcard={dc}
fetchDashCardData={this.props.fetchDashCardData}
markNewCardSeen={this.props.markNewCardSeen}
onEdit={this.onDashCardEdit.bind(this, dc)}
onRemove={this.onDashCardRemove.bind(this, dc)}
onAddSeries={this.onDashCardAddSeries.bind(this, dc)}
/>
<div className="DashCard-actions absolute top right text-brand p1">
<a href="#" onClick={() => this.onEditDashCard(dc)}>
<Icon className="m1" name="pencil" width="24" height="24" />
</a>
<a data-metabase-event="Dashboard;Remove Card Modal" href="#" onClick={() => this.setState({ removeModalDashCard: dc })}>
<Icon className="m1" name="trash" width="24" height="24" />
</a>
</div>
</div>
)}
</ResponsiveReactGridLayout>
{this.renderRemoveModal()}
{this.renderAddSeriesModal()}
</div>
);
}
......
......@@ -105,6 +105,10 @@ export var ICON_PATHS = {
},
trash: 'M4.31904507,29.7285487 C4.45843264,30.9830366 5.59537721,32 6.85726914,32 L20.5713023,32 C21.8337371,32 22.9701016,30.9833707 23.1095264,29.7285487 L25.1428571,11.4285714 L2.28571429,11.4285714 L4.31904507,29.7285487 L4.31904507,29.7285487 Z M6.85714286,4.57142857 L8.57142857,0 L18.8571429,0 L20.5714286,4.57142857 L25.1428571,4.57142857 C27.4285714,4.57142857 27.4285714,9.14285714 27.4285714,9.14285714 L13.7142857,9.14285714 L-1.0658141e-14,9.14285714 C-1.0658141e-14,9.14285714 -1.0658141e-14,4.57142857 2.28571429,4.57142857 L6.85714286,4.57142857 L6.85714286,4.57142857 Z M9.14285714,4.57142857 L18.2857143,4.57142857 L17.1428571,2.28571429 L10.2857143,2.28571429 L9.14285714,4.57142857 L9.14285714,4.57142857 Z',
unknown: 'M16.5,26.5 C22.0228475,26.5 26.5,22.0228475 26.5,16.5 C26.5,10.9771525 22.0228475,6.5 16.5,6.5 C10.9771525,6.5 6.5,10.9771525 6.5,16.5 C6.5,22.0228475 10.9771525,26.5 16.5,26.5 L16.5,26.5 Z M16.5,23.5 C12.6340068,23.5 9.5,20.3659932 9.5,16.5 C9.5,12.6340068 12.6340068,9.5 16.5,9.5 C20.3659932,9.5 23.5,12.6340068 23.5,16.5 C23.5,20.3659932 20.3659932,23.5 16.5,23.5 L16.5,23.5 Z',
warning: {
svg: '<g fill="currentcolor" fill-rule="evenodd"><path d="M10.9665007,4.7224988 C11.5372866,3.77118898 12.455761,3.75960159 13.0334993,4.7224988 L22.9665007,21.2775012 C23.5372866,22.228811 23.1029738,23 21.9950534,23 L2.00494659,23 C0.897645164,23 0.455760956,22.2403984 1.03349928,21.2775012 L10.9665007,4.7224988 Z M13.0184348,11.258 L13.0184348,14.69 C13.0184348,15.0580018 12.996435,15.4229982 12.9524348,15.785 C12.9084346,16.1470018 12.8504351,16.5159981 12.7784348,16.892 L11.5184348,16.892 C11.4464344,16.5159981 11.388435,16.1470018 11.3444348,15.785 C11.3004346,15.4229982 11.2784348,15.0580018 11.2784348,14.69 L11.2784348,11.258 L13.0184348,11.258 Z M11.0744348,19.058 C11.0744348,18.9139993 11.1014345,18.7800006 11.1554348,18.656 C11.2094351,18.5319994 11.2834343,18.4240005 11.3774348,18.332 C11.4714353,18.2399995 11.5824341,18.1670003 11.7104348,18.113 C11.8384354,18.0589997 11.978434,18.032 12.1304348,18.032 C12.2784355,18.032 12.4164341,18.0589997 12.5444348,18.113 C12.6724354,18.1670003 12.7844343,18.2399995 12.8804348,18.332 C12.9764353,18.4240005 13.0514345,18.5319994 13.1054348,18.656 C13.1594351,18.7800006 13.1864348,18.9139993 13.1864348,19.058 C13.1864348,19.2020007 13.1594351,19.3369994 13.1054348,19.463 C13.0514345,19.5890006 12.9764353,19.6979995 12.8804348,19.79 C12.7844343,19.8820005 12.6724354,19.9539997 12.5444348,20.006 C12.4164341,20.0580003 12.2784355,20.084 12.1304348,20.084 C11.978434,20.084 11.8384354,20.0580003 11.7104348,20.006 C11.5824341,19.9539997 11.4714353,19.8820005 11.3774348,19.79 C11.2834343,19.6979995 11.2094351,19.5890006 11.1554348,19.463 C11.1014345,19.3369994 11.0744348,19.2020007 11.0744348,19.058 Z"></path></g>',
attrs: { viewBox: '0 0 23 23' }
},
"illustration-icon-pie": {
svg: "<path d='M29.8065455,22.2351515 L15.7837576,15.9495758 L15.7837576,31.2174545 C22.0004848,31.2029091 27.3444848,27.5258182 29.8065455,22.2351515' fill='#78B5EC'></path><g id='Fill-1-+-Fill-3'><path d='M29.8065455,22.2351515 C30.7316364,20.2482424 31.2630303,18.0402424 31.2630303,15.7032727 C31.2630303,11.8138182 29.8220606,8.26763636 27.4569697,5.54472727 L15.7837576,15.9495758 L29.8065455,22.2351515' fill='#3875AC'></path><path d='M27.4569697,5.54472727 C24.6118788,2.26909091 20.4266667,0.188121212 15.7478788,0.188121212 C7.17963636,0.188121212 0.232727273,7.1350303 0.232727273,15.7032727 C0.232727273,24.2724848 7.17963636,31.2184242 15.7478788,31.2184242 C15.7604848,31.2184242 15.7721212,31.2174545 15.7837576,31.2174545 L15.7837576,15.9495758 L27.4569697,5.54472727' fill='#4C9DE6'></path></g>"
},
......
......@@ -20,7 +20,7 @@ export default class AreaChart extends Component {
render() {
return (
<CardRenderer className="flex-full" {...this.props} />
<CardRenderer className="flex-full" {...this.props} chartType="area" />
);
}
}
......@@ -6,7 +6,7 @@ import { MinColumnsError } from "./errors";
export default class BarChart extends Component {
static displayName = "Bar";
static identifier = "bar";
static identifier = "bar-old";
static iconName = "bar";
static isSensible(cols, rows) {
......@@ -19,7 +19,7 @@ export default class BarChart extends Component {
render() {
return (
<CardRenderer className="flex-full" {...this.props} />
<CardRenderer className="flex-full" {...this.props} chartType="bar" />
);
}
}
......@@ -10,7 +10,8 @@ import { getSettingsForVisualization, setLatitudeAndLongitude } from "metabase/l
export default class CardRenderer_ extends Component {
static propTypes = {
card: PropTypes.object.isRequired,
data: PropTypes.object
data: PropTypes.object,
chartType: PropTypes.string.isRequired
};
shouldComponentUpdate(nextProps, nextState) {
......@@ -48,33 +49,26 @@ export default class CardRenderer_ extends Component {
visualization_settings: vizSettings
};
if (this.props.card.display === "pin_map") {
if (this.props.chartType === "pin_map") {
// call signature is (elementId, card, updateMapCenter (callback), updateMapZoom (callback))
// identify the lat/lon columns from our data and make them part of the viz settings so we can render maps
card.visualization_settings = setLatitudeAndLongitude(card.visualization_settings, this.props.data.cols);
// these are example callback functions that could be passed into the renderer
// var updateMapCenter = function(lat, lon) {
// scope.card.visualization_settings.map.center_latitude = lat;
// scope.card.visualization_settings.map.center_longitude = lon;
// scope.$apply();
// };
// var updateMapZoom = function(zoom) {
// scope.card.visualization_settings.map.zoom = zoom;
// scope.$apply();
// };
var no_op = function(a, b) {
// do nothing for now
var updateMapCenter = (lat, lon) => {
this.props.onUpdateVisualizationSetting(["map", "center_latitude"], lat);
this.props.onUpdateVisualizationSetting(["map", "center_longitude"], lat);
};
var updateMapZoom = (zoom) => {
this.props.onUpdateVisualizationSetting(["map", "zoom"], zoom);
};
charting.CardRenderer[this.props.card.display](element, card, no_op, no_op);
charting.CardRenderer[this.props.chartType](element, card, updateMapCenter, updateMapZoom);
} else {
// TODO: it would be nicer if this didn't require the whole card
charting.CardRenderer[this.props.card.display](element, card, this.props.data);
charting.CardRenderer[this.props.chartType](element, card, this.props.data);
}
} catch (err) {
console.error(err);
......
......@@ -20,7 +20,7 @@ export default class LineChart extends Component {
render() {
return (
<CardRenderer className="flex-full" {...this.props} />
<CardRenderer className="flex-full" {...this.props} chartType="line" />
);
}
}
import React, { Component, PropTypes } from "react";
import ExplicitSize from "metabase/components/ExplicitSize.jsx";
import Icon from "metabase/components/Icon.jsx";
import { Bar } from "react-chartjs";
import { MinColumnsError } from "./errors";
import { formatValueString } from "metabase/lib/formatting";
const COLORS = ["#4A90E2", "#84BB4C", "#F9CF48", "#ED6E6E", "#885AB1"];
const CHART_OPTIONS = {
animation: false,
scaleShowGridLines: false,
datasetFill: true,
maintainAspectRatio: false,
barShowStroke: false,
barStrokeWidth: 2,
barValueSpacing: 5,
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
}
export default class NewBarChart extends Component {
static displayName = "Bar (chart.js)";
static identifier = "bar";
static iconName = "bar";
static noHeader = true;
static isSensible(cols, rows) {
return cols.length > 1;
}
static checkRenderable(cols, rows) {
if (cols.length < 2) { throw new MinColumnsError(2, cols.length); }
}
static defaultProps = {
series: []
};
renderLegendItem(card, index) {
return (
<span className="h3 mr2 mb1 text-bold flex align-center">
<span className="inline-block circular" style={{width: 13, height: 13, backgroundColor: COLORS[index % COLORS.length]}} />
<span className="ml1">{card.name}</span>
</span>
)
}
render() {
let { card, series } = this.props;
let headers = [];
headers.push(this.renderLegendItem(card, 0));
for (let [index, s] of series.entries()) {
headers.push(this.renderLegendItem(s.card, index + 1));
}
if (this.props.onAddSeries) {
headers.push(
<a className="h3 mr2 mb1 cursor-pointer flex align-center text-brand-hover" style={{ pointerEvents: "all" }} onClick={this.props.onAddSeries}>
<Icon className="circular bordered border-brand text-brand" style={{ padding: "0.25em" }} name="add" width={12} height={12} />
<span className="ml1">Add data</span>
</a>
);
}
return (
<div className="flex flex-full flex-column px4 py2">
<div className="Card-title my1 flex flex-no-shrink flex-row flex-wrap">{headers}</div>
<BarChart className="flex-full" {...this.props} />
</div>
);
}
}
@ExplicitSize
class BarChart extends Component {
render() {
let { width, height, data, card, series } = this.props;
let options = {
...CHART_OPTIONS,
showScale: true
};
let barData = {
labels: data.rows.map(d => formatValueString(d[0], data.cols[0])),
datasets: [
{
label: card.name || "",
fillColor: COLORS[0],
data: data.rows.map(d => d[1])
}
]
};
for (let [index, s] of series.entries()) {
barData.datasets.push({
label: s.card.name || "",
fillColor: COLORS[(index + 1) % COLORS.length],
data: s.data.rows.map(d => d[1])
});
}
return (
<Bar key={"bar_"+width+"_"+height} data={barData} options={options} width={width} height={height} redraw />
);
}
}
......@@ -19,7 +19,7 @@ export default class PieChart extends Component {
render() {
return (
<CardRenderer className="flex-full" {...this.props} />
<CardRenderer className="flex-full" {...this.props} chartType="pie" />
);
}
}
......@@ -21,7 +21,7 @@ export default class PinMap extends Component {
render() {
return (
<CardRenderer className="flex-full" {...this.props} />
<CardRenderer className="flex-full" {...this.props} chartType="pin_map" />
);
}
}
......@@ -21,7 +21,7 @@ export default class USStateMap extends Component {
render() {
return (
<CardRenderer className="flex-full" {...this.props} />
<CardRenderer className="flex-full" {...this.props} chartType="state" />
);
}
}
......@@ -40,10 +40,10 @@ export default class Visualization extends Component {
} else if (!newProps.card.display) {
this.setState({ error: "Chart type not set" });
} else {
let visualization = visualizations.get(newProps.card.display);
let CardVisualization = visualizations.get(newProps.card.display);
try {
if (visualization.checkRenderable) {
visualization.checkRenderable(newProps.data.cols, newProps.data.rows);
if (CardVisualization.checkRenderable) {
CardVisualization.checkRenderable(newProps.data.cols, newProps.data.rows);
}
this.setState({ error: null });
} catch (e) {
......@@ -72,7 +72,12 @@ export default class Visualization extends Component {
} else {
let { card } = this.props;
let CardVisualization = visualizations.get(card.display);
return <CardVisualization {...this.props} onRenderError={this.onRenderError} />;
return (
<CardVisualization {...this.props}
onUpdateVisualizationSetting={(...args) => console.log("onUpdateVisualizationSetting", args)}
onRenderError={this.onRenderError}
/>
);
}
}
}
......@@ -21,7 +21,7 @@ export default class WorldMap extends Component {
render() {
return (
<CardRenderer className="flex-full" {...this.props} />
<CardRenderer className="flex-full" {...this.props} chartType="world" />
);
}
}
......@@ -9,6 +9,8 @@ import USStateMap from "./USStateMap.jsx";
import WorldMap from "./WorldMap.jsx";
import PinMap from "./PinMap.jsx";
import NewBarChart from "./NewBarChart.jsx";
const visualizations = new Map();
export function registerVisualization(visualization) {
......@@ -26,6 +28,7 @@ registerVisualization(Scalar);
registerVisualization(Table);
registerVisualization(LineChart);
registerVisualization(BarChart);
registerVisualization(NewBarChart);
registerVisualization(PieChart);
registerVisualization(AreaChart);
registerVisualization(USStateMap);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment