Skip to content
Snippets Groups Projects
Commit fe5db887 authored by Tom Robinson's avatar Tom Robinson Committed by GitHub
Browse files

Merge pull request #2931 from metabase/chart-settings

Chart settings
parents 2d1fc87e edeb871e
No related branches found
No related tags found
No related merge requests found
Showing
with 744 additions and 434 deletions
......@@ -6,9 +6,14 @@
var http = require("http");
var httpProxy = require("http-proxy");
var url = require("url");
var backendTarget = process.argv[2] || "https://staging.metabase.com/";
var frontendTarget = process.argv[3] || "http://127.0.0.1:3000/";
var backendHost = url.parse(backendTarget).host;
var frontendHost = url.parse(frontendTarget).host;
var listenPort = parseInt(process.argv[4] || "3001");
var proxy = httpProxy.createProxyServer({ secure: false });
......@@ -16,9 +21,11 @@ var proxy = httpProxy.createProxyServer({ secure: false });
var server = http.createServer(function(req, res) {
if (/^\/app\//.test(req.url)) {
console.log("FRONTEND: " + req.url);
req.headers.host = frontendHost;
proxy.web(req, res, { target: frontendTarget });
} else {
console.log("BACKEND: " + req.url);
req.headers.host = backendHost;
proxy.web(req, res, { target: backendTarget });
}
});
......
......@@ -48,7 +48,7 @@ class BrowserSelect extends Component {
}
render() {
const { children, className, onChange, searchProp, searchCaseInsensitive, isInitiallyOpen, placeholder } = this.props;
const { className, children, value, onChange, searchProp, searchCaseInsensitive, isInitiallyOpen, placeholder } = this.props;
let selectedName;
for (const child of children) {
......@@ -78,9 +78,9 @@ class BrowserSelect extends Component {
return (
<PopoverWithTrigger
ref="popover"
className={this.props.className}
className={className}
triggerElement={
<div className={"flex align-center " + (!this.props.value ? " text-grey-3" : "")}>
<div className={"flex align-center " + (!value ? " text-grey-3" : "")}>
<span className="mr1">{selectedName}</span>
<Icon className="flex-align-right" name="chevrondown" size={12} />
</div>
......@@ -151,6 +151,7 @@ class LegacySelect extends Component {
optionNameFn: PropTypes.func,
optionValueFn: PropTypes.func,
className: PropTypes.string,
isInitiallyOpen: PropTypes.bool,
//TODO: clean up hardcoded "AdminSelect" class on trigger to avoid this workaround
triggerClasses: PropTypes.string
};
......@@ -158,7 +159,8 @@ class LegacySelect extends Component {
static defaultProps = {
placeholder: "",
optionNameFn: (option) => option.name,
optionValueFn: (option) => option
optionValueFn: (option) => option,
isInitiallyOpen: false,
};
toggle() {
......@@ -166,17 +168,19 @@ class LegacySelect extends Component {
}
render() {
var selectedName = this.props.value ? this.props.optionNameFn(this.props.value) : this.props.placeholder;
const { className, value, onChange, options, optionNameFn, optionValueFn, placeholder, isInitiallyOpen } = this.props;
var selectedName = value ? optionNameFn(value) : placeholder;
var triggerElement = (
<div className={"flex align-center " + (!this.props.value ? " text-grey-3" : "")}>
<div className={"flex align-center " + (!value ? " text-grey-3" : "")}>
<span className="mr1">{selectedName}</span>
<Icon className="flex-align-right" name="chevrondown" size={12}/>
</div>
);
var sections = {};
this.props.options.forEach(function (option) {
options.forEach(function (option) {
var sectionName = option.section || "";
sections[sectionName] = sections[sectionName] || { title: sectionName || undefined, items: [] };
sections[sectionName].items.push(option);
......@@ -185,12 +189,12 @@ class LegacySelect extends Component {
var columns = [
{
selectedItem: this.props.value,
selectedItem: value,
sections: sections,
itemTitleFn: this.props.optionNameFn,
itemTitleFn: optionNameFn,
itemDescriptionFn: (item) => item.description,
itemSelectFn: (item) => {
this.props.onChange(this.props.optionValueFn(item))
onChange(optionValueFn(item))
this.toggle();
}
}
......@@ -199,9 +203,10 @@ class LegacySelect extends Component {
return (
<PopoverWithTrigger
ref="popover"
className={this.props.className}
className={className}
triggerElement={triggerElement}
triggerClasses={this.props.triggerClasses || cx("AdminSelect", this.props.className)}
isInitiallyOpen={isInitiallyOpen}
>
<div onClick={(e) => e.stopPropagation()}>
<ColumnarSelector
......
......@@ -21,6 +21,7 @@ export default ComposedComponent => class extends Component {
this._startCheckObscured = this._startCheckObscured.bind(this);
this._stopCheckObscured = this._stopCheckObscured.bind(this);
this.onClose = this.onClose.bind(this);
}
static defaultProps = {
......@@ -91,18 +92,27 @@ export default ComposedComponent => class extends Component {
render() {
const { triggerClasses, triggerClassesOpen } = this.props;
const { isOpen } = this.state;
let { triggerElement } = this.props;
if (triggerElement && triggerElement.type === Tooltip) {
// Disables tooltip when open:
triggerElement = React.cloneElement(triggerElement, { isEnabled: triggerElement.props.isEnabled && !isOpen });
}
// if we have a single child which doesn't have an onClose prop go ahead and inject it directly
let { children } = this.props;
if (React.Children.count(children) === 1 && React.Children.only(children).props.onClose === undefined) {
children = React.cloneElement(children, { onClose: this.onClose });
}
return (
<a ref="trigger" onClick={() => this.toggle()} className={cx("no-decoration", triggerClasses, isOpen ? triggerClassesOpen : null)}>
{triggerElement}
<ComposedComponent
{...this.props}
children={children}
isOpen={isOpen}
onClose={this.onClose.bind(this)}
onClose={this.onClose}
target={() => this.target()}
/>
</a>
......
......@@ -10,6 +10,10 @@
.Modal.Modal--medium { width: 65%; }
.Modal.Modal--wide { width: 85%; }
.Modal.Modal--tall {
min-height: 85%;
}
.Modal-backdrop {
background-color: rgba(255, 255, 255, 0.6);
}
......
.hide { display: none !important; }
.show { display: inheirt; }
.hidden { visibility: hidden; }
.sm-show,
.md-show,
.lg-show,
......
......@@ -4,6 +4,9 @@ import ReactDOM from "react-dom";
import visualizations from "metabase/visualizations";
import Visualization from "metabase/visualizations/components/Visualization.jsx";
import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
import ChartSettings from "metabase/visualizations/components/ChartSettings.jsx";
import Icon from "metabase/components/Icon.jsx";
import DashCardParameterMapper from "../components/parameters/DashCardParameterMapper.jsx";
......@@ -115,7 +118,14 @@ export default class DashCard extends Component {
isDashboard={true}
isEditing={isEditing}
gridSize={this.props.isMobile ? undefined : { width: dashcard.sizeX, height: dashcard.sizeY }}
actionButtons={isEditing && !isEditingParameter ? <DashCardActionButtons series={series} visualization={CardVisualization} onRemove={onRemove} onAddSeries={onAddSeries} /> : undefined}
actionButtons={isEditing && !isEditingParameter ?
<DashCardActionButtons
series={series}
visualization={CardVisualization}
onRemove={onRemove}
onAddSeries={onAddSeries}
/> : undefined
}
onUpdateVisualizationSetting={this.props.onUpdateVisualizationSetting}
replacementContent={isEditingParameter && <DashCardParameterMapper dashcard={dashcard} />}
/>
......@@ -124,21 +134,40 @@ export default class DashCard extends Component {
}
}
const DashCardActionButtons = ({ series, visualization, onRemove, onAddSeries }) =>
const DashCardActionButtons = ({ series, visualization, onRemove, onAddSeries, onUpdateVisualizationSettings }) =>
<span className="DashCard-actions flex align-center">
{ visualization.supportsSeries &&
<AddSeriesButton series={series} onAddSeries={onAddSeries} />
}
{ onUpdateVisualizationSettings &&
<ChartSettingsButton series={series} onChange={onUpdateVisualizationSettings} />
}
<RemoveButton onRemove={onRemove} />
</span>
const ChartSettingsButton = ({ series, onUpdateVisualizationSettings }) =>
<ModalWithTrigger
className="Modal Modal--wide Modal--tall"
triggerElement={<Icon name="gear" />}
triggerClasses="text-grey-2 text-grey-4-hover cursor-pointer mr1 flex align-center flex-no-shrink"
>
<ChartSettings
series={series}
onChange={onUpdateVisualizationSettings}
/>
</ModalWithTrigger>
const RemoveButton = ({ onRemove }) =>
<a className="text-grey-2 text-grey-4-hover expand-clickable" data-metabase-event="Dashboard;Remove Card Modal" href="#" onClick={onRemove}>
<Icon name="close" size={14} />
</a>
const AddSeriesButton = ({ series, onAddSeries }) =>
<a data-metabase-event={"Dashboard;Edit Series Modal;open"} className="text-grey-2 text-grey-4-hover cursor-pointer h3 ml1 mr2 flex align-center flex-no-shrink relative" onClick={onAddSeries}>
<a
data-metabase-event={"Dashboard;Edit Series Modal;open"}
className="text-grey-2 text-grey-4-hover cursor-pointer h3 ml1 mr2 flex align-center flex-no-shrink relative"
onClick={onAddSeries}
>
<Icon className="absolute" style={{ top: 2, left: 2 }} name="add" size={8} />
<Icon name={getSeriesIconName(series)} size={12} />
<span className="flex-no-shrink">{ series.length > 1 ? "Edit" : "Add" }</span>
......
......@@ -12,15 +12,14 @@ import NewUserOnboardingModal from '../components/NewUserOnboardingModal.jsx';
import NextStep from "../components/NextStep.jsx";
import * as homepageActions from "../actions";
import { getActivity, getRecentViews, getUser, getShowOnboarding } from "../selectors";
const mapStateToProps = (state, props) => {
return {
activity: state.home && state.home.activity,
recentViews: state.home && state.home.recentViews,
user: state.currentUser,
showOnboarding: state.router && state.router.location && "new" in state.router.location.query
// onChangeLocation
activity: getActivity(state),
recentViews: getRecentViews(state),
user: getUser(state),
showOnboarding: getShowOnboarding(state)
}
}
......
import { createSelector } from 'reselect';
export const homepageSelectors = createSelector(
[state => state.activity,
state => state.recentViews],
(activity, recentViews) => ({activity, recentViews})
);
export const getActivity = (state) => state.home && state.home.activity
export const getRecentViews = (state) => state.home && state.home.recentViews
export const getUser = (state) => state.currentUser
export const getShowOnboarding = (state) => state.router && state.router.location && "new" in state.router.location.query
......@@ -84,6 +84,7 @@ export var ICON_PATHS = {
funneladd: 'M22.5185184,5.27947653 L17.2510286,5.27947653 L17.2510286,9.50305775 L22.5185184,9.50305775 L22.5185184,14.7825343 L26.7325102,14.7825343 L26.7325102,9.50305775 L32,9.50305775 L32,5.27947653 L26.7325102,5.27947653 L26.7325102,0 L22.5185184,0 L22.5185184,5.27947653 Z M14.9369872,0.791920724 C14.9369872,0.791920724 2.77552871,0.83493892 1.86648164,0.83493892 C0.957434558,0.83493892 0.45215388,1.50534608 0.284450368,1.77831828 C0.116746855,2.05129048 -0.317642562,2.91298361 0.398382661,3.9688628 C1.11440788,5.024742 9.74577378,17.8573356 9.74577378,17.8573356 C9.74577378,17.8573356 9.74577394,28.8183645 9.74577378,29.6867194 C9.74577362,30.5550744 9.83306175,31.1834301 10.7557323,31.6997692 C11.6784029,32.2161084 12.4343349,31.9564284 12.7764933,31.7333621 C13.1186517,31.5102958 19.6904355,27.7639669 20.095528,27.4682772 C20.5006204,27.1725875 20.7969652,26.5522071 20.7969651,25.7441659 C20.7969649,24.9361247 20.7969651,18.2224765 20.7969651,18.2224765 L21.6163131,16.9859755 L18.152048,15.0670739 C18.152048,15.0670739 17.3822517,16.199685 17.2562629,16.4000338 C17.1302741,16.6003826 16.8393552,16.9992676 16.8393551,17.7062886 C16.8393549,18.4133095 16.8393551,24.9049733 16.8393551,24.9049733 L13.7519708,26.8089871 C13.7519708,26.8089871 13.7318369,18.3502323 13.7318367,17.820601 C13.7318366,17.2909696 13.8484216,16.6759061 13.2410236,15.87149 C12.6336257,15.0670739 5.59381579,4.76288686 5.59381579,4.76288686 L14.9359238,4.76288686 L14.9369872,0.791920724 Z',
folder: "M3.96901618e-15,5.41206355 L0.00949677904,29 L31.8821132,29 L31.8821132,10.8928571 L18.2224205,10.8928571 L15.0267944,5.41206355 L3.96901618e-15,5.41206355 Z M16.8832349,5.42402804 L16.8832349,4.52140947 C16.8832349,3.68115822 17.5639241,3 18.4024298,3 L27.7543992,3 L30.36417,3 C31.2031259,3 31.8832341,3.67669375 31.8832341,4.51317691 L31.8832341,7.86669975 L31.8832349,8.5999999 L18.793039,8.5999999 L16.8832349,5.42402804 Z",
gear: 'M14 0 H18 L19 6 L20.707 6.707 L26 3.293 L28.707 6 L25.293 11.293 L26 13 L32 14 V18 L26 19 L25.293 20.707 L28.707 26 L26 28.707 L20.707 25.293 L19 26 L18 32 L14 32 L13 26 L11.293 25.293 L6 28.707 L3.293 26 L6.707 20.707 L6 19 L0 18 L0 14 L6 13 L6.707 11.293 L3.293 6 L6 3.293 L11.293 6.707 L13 6 L14 0 z M16 10 A6 6 0 0 0 16 22 A6 6 0 0 0 16 10',
grabber: 'M0,5 L32,5 L32,9.26666667 L0,9.26666667 L0,5 Z M0,13.5333333 L32,13.5333333 L32,17.8 L0,17.8 L0,13.5333333 Z M0,22.0666667 L32,22.0666667 L32,26.3333333 L0,26.3333333 L0,22.0666667 Z',
grid: 'M2 2 L10 2 L10 10 L2 10z M12 2 L20 2 L20 10 L12 10z M22 2 L30 2 L30 10 L22 10z M2 12 L10 12 L10 20 L2 20z M12 12 L20 12 L20 20 L12 20z M22 12 L30 12 L30 20 L22 20z M2 22 L10 22 L10 30 L2 30z M12 22 L20 22 L20 30 L12 30z M22 22 L30 22 L30 30 L22 30z',
google: {
svg: '<g fill="none" fill-rule="evenodd"><path d="M16 32c4.32 0 7.947-1.422 10.596-3.876l-5.05-3.91c-1.35.942-3.164 1.6-5.546 1.6-4.231 0-7.822-2.792-9.102-6.65l-5.174 4.018C4.356 28.41 9.742 32 16 32z" fill="#34A853"/><path d="M6.898 19.164A9.85 9.85 0 0 1 6.364 16c0-1.102.196-2.169.516-3.164L1.707 8.818A16.014 16.014 0 0 0 0 16c0 2.578.622 5.013 1.707 7.182l5.19-4.018z" fill="#FBBC05"/><path d="M31.36 16.356c0-1.316-.107-2.276-.338-3.272H16v5.938h8.818c-.178 1.476-1.138 3.698-3.271 5.191l5.049 3.911c3.022-2.79 4.764-6.897 4.764-11.768z" fill="#4285F4"/><path d="M16 6.187c3.004 0 5.031 1.297 6.187 2.382l4.515-4.409C23.93 1.582 20.32 0 16 0 9.742 0 4.338 3.591 1.707 8.818l5.173 4.018c1.298-3.858 4.889-6.65 9.12-6.65z" fill="#EA4335"/></g>'
......
......@@ -107,7 +107,7 @@ export const isSummable = isFieldType.bind(null, SUMMABLE);
export const isCategory = isFieldType.bind(null, CATEGORY);
export const isDimension = (col) => (col && col.source !== "aggregation");
export const isMetric = (col) => (col && col.source !== "breakout") && isNumeric(col);
export const isMetric = (col) => (col && col.source !== "breakout") && isSummable(col);
export const isNumericBaseType = (field) => TYPES[NUMBER].base
.some(type => type === field.base_type);
......
This diff is collapsed.
/* @flow */
import Metadata from "./Metadata";
import Database from "./Database";
async function getDatabases() {
let response = await fetch("/api/database?include_tables=true", { credentials: 'same-origin' });
return await response.json();
}
async function getTable(table) {
let response = await fetch("/api/table/" + table.id + "/query_metadata", { credentials: 'same-origin' });
return await response.json();
}
async function loadDatabaseTables(database) {
database.tables = await Promise.all(database.tables.map(getTable));
}
async function loadMetadata() {
let databases = await getDatabases();
await Promise.all(databases.map(loadDatabaseTables));
return databases;
}
loadMetadata().then((databases) => {
window.m = new Metadata(databases);
window.d = new Database(databases[0]);
console.log(window.m.databases());
console.log(window.m.databases()[1].tables()[0].field(1835).target().table().database().tables()[0].fields()[0].isNumeric());
console.log(window.d.tables());
}).then(undefined, (err) => console.error(err))
......@@ -5,9 +5,13 @@ import type { StructuredQueryObject, NativeQueryObject } from "./Query";
export type CardId = number;
export type VisualizationSettings = { [key: string]: any }
export type CardObject = {
id: CardId,
dataset_query: DatasetQueryObject
dataset_query: DatasetQueryObject,
display: string,
visualization_settings: VisualizationSettings
};
export type StructuredDatasetQueryObject = {
......
......@@ -2,6 +2,9 @@ import React, { Component, PropTypes } from "react";
import Icon from "metabase/components/Icon.jsx";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
import ChartSettings from "metabase/visualizations/components/ChartSettings.jsx";
import visualizations from "metabase/visualizations";
......@@ -10,7 +13,6 @@ import cx from "classnames";
export default class VisualizationSettings extends React.Component {
constructor(props, context) {
super(props, context);
this.setDisplay = this.setDisplay.bind(this);
}
static propTypes = {
......@@ -21,7 +23,7 @@ export default class VisualizationSettings extends React.Component {
onUpdateVisualizationSettings: PropTypes.func.isRequired
};
setDisplay(type) {
setDisplay = (type) => {
// notify our parent about our change
this.props.setDisplayFn(type);
this.refs.displayPopover.toggle();
......@@ -88,6 +90,16 @@ export default class VisualizationSettings extends React.Component {
return (
<div className="VisualizationSettings flex align-center">
{this.renderChartTypePicker()}
<ModalWithTrigger
className="Modal Modal--wide Modal--tall"
triggerElement={<Icon name="gear" />}
triggerClasses="text-brand-hover"
>
<ChartSettings
series={[{ card: this.props.card, data: this.props.result.data }]}
onChange={this.props.onUpdateVisualizationSettings}
/>
</ModalWithTrigger>
{this.renderVisualizationSettings()}
</div>
);
......
......@@ -759,9 +759,7 @@ export const queryCompleted = createThunkAction(QUERY_COMPLETED, (card, queryRes
// any time we were a scalar and now have more than 1x1 data switch to table view
cardDisplay = "table";
} else if (Query.isStructured(card.dataset_query) &&
Query.isBareRowsAggregation(card.dataset_query.query) &&
card.display !== "pin_map") {
} else if (!card.display) {
// if our query aggregation is "rows" then ALWAYS set the display to "table"
cardDisplay = "table";
}
......
import React, { Component, PropTypes } from 'react';
import cx from "classnames";
import Icon from "metabase/components/Icon.jsx";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
// TODO: since these are shared we should probably find somewhere else to keep them
import DateMonthYearWidget from "metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx";
import DateQuarterYearWidget from "metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx";
import DateRangeWidget from "metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx";
import DateRelativeWidget from "metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx";
import DateSingleWidget from "metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx";
import CategoryWidget from "metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx";
import TextWidget from "metabase/dashboard/components/parameters/widgets/TextWidget.jsx";
export default class TagValuePicker extends Component {
static propTypes = {
parameter: PropTypes.object.isRequired,
value: PropTypes.any,
values: PropTypes.array,
setValue: PropTypes.func.isRequired
};
static defaultProps = {
value: null,
values: []
};
determinePickerComponent(type, numValues) {
switch(type) {
case null: return UnknownWidget;
case "date/month-year": return DateMonthYearWidget;
case "date/quarter-year": return DateQuarterYearWidget;
case "date/range": return DateRangeWidget;
case "date/relative": return DateRelativeWidget;
case "date/single": return DateSingleWidget;
default: if (numValues > 0) {
return CategoryWidget;
} else {
return TextWidget;
}
}
}
render() {
const { parameter, setValue, value, values } = this.props;
const hasValue = value != null;
const placeholder = "Select…";
// determine the correct Picker to render based on the parameter data type
const PickerComponent = this.determinePickerComponent(parameter.type, values.length);
if (PickerComponent.noPopover) {
let classNames = cx("px1 flex align-center bordered border-med rounded TagValuePickerNoPopover", {
"text-bold": hasValue,
"text-grey-4": !hasValue,
"text-brand": hasValue,
"border-brand": hasValue,
"TagValuePickerNoPopover--selected": hasValue
});
return (
<div style={{paddingTop: "0.25rem", paddingBottom: "0.25rem"}} className={classNames}>
<PickerComponent value={value} values={values} setValue={setValue} />
{ hasValue &&
<Icon name="close" className="flex-align-right cursor-pointer" onClick={(e) => {
if (hasValue) {
e.stopPropagation();
setValue(null);
}
}} />
}
</div>
);
}
let classNames = cx("p1 flex align-center bordered border-med rounded", {
"text-bold": hasValue,
"text-grey-4": !hasValue,
"text-brand": hasValue,
"border-brand": hasValue
});
return (
<PopoverWithTrigger
ref="valuePopover"
triggerElement={
<div ref="trigger" style={{minHeight: 36, minWidth: 150}} className={classNames}>
<div className="mr1">{ hasValue ? PickerComponent.format(value) : placeholder }</div>
<Icon name={hasValue ? "close" : "chevrondown"} className="flex-align-right" onClick={(e) => {
if (hasValue) {
e.stopPropagation();
setValue(null);
}
}}/>
</div>
}
target={() => this.refs.trigger} // not sure why this is necessary
>
<PickerComponent
value={value}
values={values}
setValue={setValue}
onClose={() => this.refs.valuePopover.close()}
/>
</PopoverWithTrigger>
);
}
}
const UnknownWidget = () =>
<input type="text" value="No type chosen" disabled={true} />
UnknownWidget.noPopover = true;
......@@ -25,3 +25,5 @@ import 'ace/snippets/mysql';
import 'ace/snippets/pgsql';
import 'ace/snippets/sqlserver';
import 'ace/snippets/json';
import 'number-to-locale-string';
import React, { Component, PropTypes } from "react";
import ChoroplethMap from "./components/ChoroplethMap.jsx";
import PinMap from "./PinMap.jsx";
import { ChartSettingsError } from "metabase/visualizations/lib/errors";
export default class Map extends Component {
static displayName = "Map";
static identifier = "map";
static iconName = "pinmap";
static aliases = ["state", "country", "pin_map"];
static minSize = { width: 4, height: 4 };
static isSensible(cols, rows) {
return true;
}
static checkRenderable(cols, rows, settings) {
if (settings["map.type"] === "pin") {
if (!settings["map.longitude_column"] || !settings["map.latitude_column"]) {
throw new ChartSettingsError("Please select longitude and latitude columns in the chart settings.", "Data");
}
} else if (settings["map.type"] === "region"){
if (!settings["map.dimension"] || !settings["map.metric"]) {
throw new ChartSettingsError("Please select region and metric columns in the chart settings.", "Data");
}
}
}
render() {
const { settings } = this.props;
const type = settings["map.type"];
if (type === "pin") {
return <PinMap {...this.props} />
} else if (type === "region") {
return <ChoroplethMap {...this.props} />
}
}
}
......@@ -5,7 +5,7 @@ import styles from "./PieChart.css";
import ChartTooltip from "./components/ChartTooltip.jsx";
import ChartWithLegend from "./components/ChartWithLegend.jsx";
import { MinColumnsError } from "metabase/visualizations/lib/errors";
import { ChartSettingsError } from "metabase/visualizations/lib/errors";
import { getFriendlyName } from "metabase/visualizations/lib/utils";
import { formatValue } from "metabase/lib/formatting";
......@@ -35,8 +35,10 @@ export default class PieChart extends Component {
return cols.length === 2;
}
static checkRenderable(cols, rows) {
if (cols.length < 2) { throw new MinColumnsError(2, cols.length); }
static checkRenderable(cols, rows, settings) {
if (!settings["pie.dimension"] || !settings["pie.metric"]) {
throw new ChartSettingsError("Please select columns in the chart settings.", "Data");
}
}
componentDidUpdate() {
......@@ -50,23 +52,26 @@ export default class PieChart extends Component {
}
render() {
const { series, hovered, onHoverChange, className, gridSize } = this.props;
const { data } = series[0];
const { series, hovered, onHoverChange, className, gridSize, settings } = this.props;
const [{ data: { cols, rows }}] = series;
const dimensionIndex = _.findIndex(cols, (col) => col.name === settings["pie.dimension"]);
const metricIndex = _.findIndex(cols, (col) => col.name === settings["pie.metric"]);
const formatDimension = (dimension, jsx = true) => formatValue(dimension, { column: data.cols[0], jsx, majorWidth: 0 })
const formatMetric = (metric, jsx = true) => formatValue(metric, { column: data.cols[1], jsx, majorWidth: 0 })
const formatDimension = (dimension, jsx = true) => formatValue(dimension, { column: cols[dimensionIndex], jsx, majorWidth: 0 })
const formatMetric = (metric, jsx = true) => formatValue(metric, { column: cols[metricIndex], jsx, majorWidth: 0 })
const formatPercent = (percent) => (100 * percent).toFixed(2) + "%"
let total = data.rows.reduce((sum, row) => sum + row[1], 0);
let total = rows.reduce((sum, row) => sum + row[metricIndex], 0);
// use standard colors for up to 5 values otherwise use color harmony to help differentiate slices
let sliceColors = Object.values(data.rows.length > 5 ? colors.harmony : colors.normal);
let sliceColors = Object.values(rows.length > 5 ? colors.harmony : colors.normal);
let [slices, others] = _.chain(data.rows)
.map(([key, value], index) => ({
key,
value,
percentage: value / total,
let [slices, others] = _.chain(rows)
.map((row, index) => ({
key: row[dimensionIndex],
value: row[metricIndex],
percentage: row[metricIndex] / total,
color: sliceColors[index % sliceColors.length]
}))
.partition((d) => d.percentage > SLICE_THRESHOLD)
......@@ -91,7 +96,7 @@ export default class PieChart extends Component {
let legendTitles = slices.map(slice => [
slice.key === "Other" ? slice.key : formatDimension(slice.key, false),
formatPercent(slice.percentage)
settings["pie.show_legend_perecent"] ? formatPercent(slice.percentage) : undefined
]);
let legendColors = slices.map(slice => slice.color);
......@@ -112,8 +117,8 @@ export default class PieChart extends Component {
value: formatMetric(o.value, false)
}))
: [
{ key: getFriendlyName(data.cols[0]), value: formatDimension(slices[index].key) },
{ key: getFriendlyName(data.cols[1]), value: formatMetric(slices[index].value) },
{ key: getFriendlyName(cols[dimensionIndex]), value: formatDimension(slices[index].key) },
{ key: getFriendlyName(cols[metricIndex]), value: formatMetric(slices[index].value) },
{ key: "Percentage", value: formatPercent(slices[index].percentage) }
]
});
......@@ -133,6 +138,7 @@ export default class PieChart extends Component {
legendTitles={legendTitles} legendColors={legendColors}
gridSize={gridSize}
hovered={hovered} onHoverChange={(d) => onHoverChange && onHoverChange(d && { ...d, ...hoverForIndex(d.index) })}
showLegend={settings["pie.show_legend"]}
>
<div className={styles.ChartAndDetail}>
<div ref="detail" className={styles.Detail}>
......
......@@ -3,7 +3,6 @@
import React, { Component, PropTypes } from "react";
import ReactDOM from "react-dom";
import { getSettingsForVisualization, setLatitudeAndLongitude } from "metabase/lib/visualization_settings";
import { hasLatitudeAndLongitudeColumns } from "metabase/lib/schema_metadata";
import { LatitudeLongitudeError } from "metabase/visualizations/lib/errors";
......@@ -55,29 +54,29 @@ export default class PinMap extends Component {
this.setState({ zoom });
}
getTileUrl(settings, coord, zoom) {
let query = this.props.series[0].card.dataset_query;
getLatLongIndexes() {
const { settings, series: [{ data: { cols }}] } = this.props;
return {
latitudeIndex: _.findIndex(cols, (col) => col.name === settings["map.latitude_column"]),
longitudeIndex: _.findIndex(cols, (col) => col.name === settings["map.longitude_column"])
};
}
let latitude_dataset_col_index = settings.map.latitude_dataset_col_index;
let longitude_dataset_col_index = settings.map.longitude_dataset_col_index;
let latitude_source_table_field_id = settings.map.latitude_source_table_field_id;
let longitude_source_table_field_id = settings.map.longitude_source_table_field_id;
getTileUrl = (coord, zoom) => {
const [{ card: { dataset_query }, data: { cols }}] = this.props.series;
if (latitude_dataset_col_index == null || longitude_dataset_col_index == null) {
return;
}
const { latitudeIndex, longitudeIndex } = this.getLatLongIndexes();
const latitudeField = cols[latitudeIndex];
const longitudeField = cols[longitudeIndex];
if (latitude_source_table_field_id == null || longitude_source_table_field_id == null) {
throw ("Map ERROR: latitude and longitude column indices must be specified");
}
if (latitude_dataset_col_index == null || longitude_dataset_col_index == null) {
throw ("Map ERROR: unable to find specified latitude / longitude columns in source table");
if (!latitudeField || !longitudeField) {
return;
}
return '/api/tiles/' + zoom + '/' + coord.x + '/' + coord.y + '/' +
latitude_source_table_field_id + '/' + longitude_source_table_field_id + '/' +
latitude_dataset_col_index + '/' + longitude_dataset_col_index + '/' +
'?query=' + encodeURIComponent(JSON.stringify(query))
latitudeField.id + '/' + longitudeField.id + '/' +
latitudeIndex + '/' + longitudeIndex + '/' +
'?query=' + encodeURIComponent(JSON.stringify(dataset_query))
}
componentDidMount() {
......@@ -87,31 +86,27 @@ export default class PinMap extends Component {
}
try {
let element = ReactDOM.findDOMNode(this.refs.map);
let { card, data } = this.props.series[0];
let settings = card.visualization_settings;
settings = getSettingsForVisualization(settings, "pin_map");
settings = setLatitudeAndLongitude(settings, data.cols);
let mapOptions = {
zoom: settings.map.zoom,
center: new google.maps.LatLng(settings.map.center_latitude, settings.map.center_longitude),
const element = ReactDOM.findDOMNode(this.refs.map);
const { settings, series: [{ data }] } = this.props;
const mapOptions = {
zoom: settings["map.zoom"],
center: new google.maps.LatLng(
settings["map.center_latitude"],
settings["map.center_longitude"]
),
mapTypeId: google.maps.MapTypeId.MAP,
scrollwheel: false
};
let map = this.map = new google.maps.Map(element, mapOptions);
const map = this.map = new google.maps.Map(element, mapOptions);
if (data.rows.length < 2000) {
let tooltip = new google.maps.InfoWindow();
let latColIndex = settings.map.latitude_dataset_col_index;
let lonColIndex = settings.map.longitude_dataset_col_index;
let { latitudeIndex, longitudeIndex } = this.getLatLongIndexes();
for (let row of data.rows) {
let marker = new google.maps.Marker({
position: new google.maps.LatLng(row[latColIndex], row[lonColIndex]),
position: new google.maps.LatLng(row[latitudeIndex], row[longitudeIndex]),
map: map,
icon: "/app/img/pin.png"
});
......@@ -124,7 +119,7 @@ export default class PinMap extends Component {
}
} else {
map.overlayMapTypes.push(new google.maps.ImageMapType({
getTileUrl: this.getTileUrl.bind(this, settings),
getTileUrl: this.getTileUrl,
tileSize: new google.maps.Size(256, 256)
}));
}
......
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