diff --git a/frontend/interfaces/icepick.js b/frontend/interfaces/icepick.js index 3c120f77394a4cbe495e47064c84399657738997..89eec22f31d3e40362d3e45e6332abf08fcdc98d 100644 --- a/frontend/interfaces/icepick.js +++ b/frontend/interfaces/icepick.js @@ -2,16 +2,16 @@ type Key = string | number; type Value = any; declare module icepick { - declare function assoc<O:Object, K:Key, V:Value>(object: O, key: K, value: V): O; - declare function dissoc<O:Object, K:Key, V:Value>(object: O, key: K): O; + declare function assoc<O:Object|Array<any>, K:Key, V:Value>(object: O, key: K, value: V): O; + declare function dissoc<O:Object|Array<any>, K:Key, V:Value>(object: O, key: K): O; - declare function getIn<O:Object, K:Key, V:Value>(object: ?O, path: Array<K>): ?V; - declare function setIn<O:Object, K:Key, V:Value>(object: O, path: Array<K>, value: V): O; - declare function assocIn<O:Object, K:Key, V:Value>(object: O, path: Array<K>, value: V): O; - declare function updateIn<O:Object, K:Key, V:Value>(object: O, path: Array<K>, callback: ((value: V) => V)): O; + declare function getIn<O:Object|Array<any>, K:Key, V:Value>(object: ?O, path: Array<K>): ?V; + declare function setIn<O:Object|Array<any>, K:Key, V:Value>(object: O, path: Array<K>, value: V): O; + declare function assocIn<O:Object|Array<any>, K:Key, V:Value>(object: O, path: Array<K>, value: V): O; + declare function updateIn<O:Object|Array<any>, K:Key, V:Value>(object: O, path: Array<K>, callback: ((value: V) => V)): O; - declare function merge<O:Object>(object: O, other: O): O; + declare function merge<O:Object|Array<any>>(object: O, other: O): O; // TODO: improve this - declare function chain<O:Object>(object: O): any; + declare function chain<O:Object|Array<any>>(object: O): any; } diff --git a/frontend/interfaces/underscore.js b/frontend/interfaces/underscore.js index c6c3210fe8c8aad3c78f660e60759441d95b318d..00fc5ce0ede504644934323c81eeccf8a3775d2f 100644 --- a/frontend/interfaces/underscore.js +++ b/frontend/interfaces/underscore.js @@ -8,7 +8,7 @@ declare module "underscore" { declare function clone<T>(obj: T): T; declare function isEqual(a: any, b: any): boolean; - declare function range(a: number, b: number): Array<number>; + declare function range(a: number, b?: number): Array<number>; declare function extend<S, T>(o1: S, o2: T): S & T; declare function zip<S, T>(a1: S[], a2: T[]): Array<[S, T]>; @@ -44,13 +44,20 @@ declare module "underscore" { declare function min<T>(a: Array<T>|{[key:any]: T}): T; declare function max<T>(a: Array<T>|{[key:any]: T}): T; - declare function uniq<T>(a: T[], isSorted?: boolean, iteratee?: (val: T) => boolean): T[]; + declare function uniq<T>(a: T[], iteratee?: (val: T) => boolean): T[]; + declare function uniq<T>(a: T[], isSorted: boolean, iteratee?: (val: T) => boolean): T[]; declare function values<T>(o: {[key: any]: T}): T[]; - declare function omit<T>(o: {[key: any]: T}, ...properties: string[]): T; + declare function omit(o: {[key: any]: any}, ...properties: string[]): {[key: any]: any}; + declare function omit(o: {[key: any]: any}, predicate: (val: any, key: any, object: {[key: any]: any})=>boolean): {[key: any]: any}; + declare function pick(o: {[key: any]: any}, ...properties: string[]): {[key: any]: any}; + declare function pick(o: {[key: any]: any}, predicate: (val: any, key: any, object: {[key: any]: any})=>boolean): {[key: any]: any}; + declare function pluck(o: Array<{[key: any]: any}>, propertyNames: string): Array<any>; declare function flatten(a: Array<any>): Array<any>; + declare function debounce<T: (any) => any>(func: T): T; + // TODO: improve this declare function chain<S>(obj: S): any; } diff --git a/frontend/src/metabase/App.jsx b/frontend/src/metabase/App.jsx index 3c28474c21b0e2599b8232d68c7484da8e1348ea..6241a1b719a2ca21fb9c99c37b316745ba36ff11 100644 --- a/frontend/src/metabase/App.jsx +++ b/frontend/src/metabase/App.jsx @@ -1,3 +1,5 @@ +/* @flow weak */ + import React, { Component, PropTypes } from "react"; import Navbar from "metabase/nav/containers/Navbar.jsx"; diff --git a/frontend/src/metabase/auth/auth.js b/frontend/src/metabase/auth/auth.js index 9cf967b8a6f7ef53df0a498f3abe18d9fa510093..fbd5aaec514b3b9fdd187a1c8c473177edc60a87 100644 --- a/frontend/src/metabase/auth/auth.js +++ b/frontend/src/metabase/auth/auth.js @@ -9,7 +9,7 @@ import MetabaseAnalytics from "metabase/lib/analytics"; import { clearGoogleAuthCredentials } from "metabase/lib/auth"; -import { refreshCurrentUser } from "metabase/user"; +import { refreshCurrentUser } from "metabase/redux/user"; import { SessionApi } from "metabase/services"; diff --git a/frontend/src/metabase/debug.js b/frontend/src/metabase/debug.js index a719d7a29fe0a5a0adcfa1b120f1ef03f7a78453..829b892496f9d9ec0b2301072b95a04f83c48dd5 100644 --- a/frontend/src/metabase/debug.js +++ b/frontend/src/metabase/debug.js @@ -1,3 +1,5 @@ +/* @flow weak */ + import React from "react"; import { Route } from "react-router"; diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js index 3fd20af20aa70d51c2ab899c9b350b636fb090a1..1d63f18c9a918c3770ba15235811e4d5308c20f6 100644 --- a/frontend/src/metabase/icon_paths.js +++ b/frontend/src/metabase/icon_paths.js @@ -1,3 +1,4 @@ +/* @flow weak */ /* Metabase Icon Paths @@ -238,6 +239,7 @@ export var ICON_PATHS = { } }; +// $FlowFixMe ICON_PATHS["illustration-line"] = ICON_PATHS['illustration-area']; export function loadIcon(name) { diff --git a/frontend/src/metabase/lib/api.js b/frontend/src/metabase/lib/api.js index 2361c2d8d99cf0c8d970713a0bfaf7ded725e2bc..53369187fac44790b8b79d009a1e9b01b8b2909e 100644 --- a/frontend/src/metabase/lib/api.js +++ b/frontend/src/metabase/lib/api.js @@ -20,8 +20,8 @@ function makeMethod(method: string, hasBody: boolean = false) { params = {}; } return function( - data: { [key:string]: any }, - options: { [key:string]: any } = {} + data?: { [key:string]: any }, + options?: { [key:string]: any } = {} ): Promise<any> { let url = urlTemplate; data = { ...data }; diff --git a/frontend/src/metabase/meta/types/Dataset.js b/frontend/src/metabase/meta/types/Dataset.js index 5ad43b5b6ad1560d4bc3094cf251091c0eb433cc..4f8c9f95e4671f1a389bd451a63ccef66218058e 100644 --- a/frontend/src/metabase/meta/types/Dataset.js +++ b/frontend/src/metabase/meta/types/Dataset.js @@ -12,17 +12,19 @@ export type Column = { display_name: string, base_type: string, special_type: ?string -} +}; export type ISO8601Times = string; export type Value = string|number|ISO8601Times|boolean|null|{}; export type Row = Value[]; +export type DatasetData = { + cols: Column[], + columns: ColumnName[], + rows: Row[] +}; + export type Dataset = { - data: { - cols: Column[], - columns: ColumnName[], - rows: Row[] - }, + data: DatasetData, json_query: DatasetQuery -} +}; diff --git a/frontend/src/metabase/reducers.js b/frontend/src/metabase/reducers.js index 0cd9ca0a5ee93f9e3aa771e70cfe9829de349406..b8339147f9bcc360653705c1b914aaacd4c5e21f 100644 --- a/frontend/src/metabase/reducers.js +++ b/frontend/src/metabase/reducers.js @@ -1,3 +1,4 @@ +/* @flow weak */ import { combineReducers } from 'redux'; @@ -36,7 +37,7 @@ import * as setup from "metabase/setup/reducers"; /* user */ import * as user from "metabase/user/reducers"; -import { currentUser } from "metabase/user"; +import { currentUser } from "metabase/redux/user"; const reducers = { // global reducers diff --git a/frontend/src/metabase/redux/requests.js b/frontend/src/metabase/redux/requests.js index 4b50b6d0b05da594481d006059a969932bb9ec0d..0458ab62d857e61c9ec8e71ac395be7af40c0b70 100644 --- a/frontend/src/metabase/redux/requests.js +++ b/frontend/src/metabase/redux/requests.js @@ -1,3 +1,5 @@ +/* @flow weak */ + import { handleActions, createAction } from "metabase/lib/redux"; import i from "icepick"; diff --git a/frontend/src/metabase/redux/settings.js b/frontend/src/metabase/redux/settings.js index 21f5d3d89668bc462104a39af4c10f10b65fe1b2..2e8e3ed1f00372ddad468595a68348cfd8ee3738 100644 --- a/frontend/src/metabase/redux/settings.js +++ b/frontend/src/metabase/redux/settings.js @@ -1,3 +1,5 @@ +/* @flow weak */ + import { createThunkAction } from "metabase/lib/redux"; import MetabaseSettings from "metabase/lib/settings"; diff --git a/frontend/src/metabase/redux/undo.js b/frontend/src/metabase/redux/undo.js index 9d50538ac0986d7cfb3311a31892763c89acdbe4..f1ffb2d649def870ec9b90c33062c7eb81280ecc 100644 --- a/frontend/src/metabase/redux/undo.js +++ b/frontend/src/metabase/redux/undo.js @@ -1,3 +1,4 @@ +/* @flow weak */ import { createAction, createThunkAction } from "metabase/lib/redux"; diff --git a/frontend/src/metabase/user.js b/frontend/src/metabase/redux/user.js similarity index 98% rename from frontend/src/metabase/user.js rename to frontend/src/metabase/redux/user.js index 68d3019ecccc1863186b5aceed4e25c6772c80c2..8425eeb378905f6352d74e84477319f21b425f49 100644 --- a/frontend/src/metabase/user.js +++ b/frontend/src/metabase/redux/user.js @@ -1,3 +1,5 @@ +/* @flow */ + import { createAction } from "redux-actions"; import { handleActions } from 'redux-actions'; diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx index 85769988f17a5069c08762dc3e086fdaa418ea06..db5b5e1307c1d5b1ce39a70385eaf25710371fe2 100644 --- a/frontend/src/metabase/routes.jsx +++ b/frontend/src/metabase/routes.jsx @@ -1,10 +1,12 @@ +/* @flow weak */ + import React, { Component, PropTypes } from "react"; import { Route, Redirect, IndexRedirect, IndexRoute } from 'react-router'; import { routerActions } from 'react-router-redux'; import { UserAuthWrapper } from 'redux-auth-wrapper'; -import { refreshCurrentUser } from "metabase/user"; +import { refreshCurrentUser } from "metabase/redux/user"; import MetabaseSettings from "metabase/lib/settings"; import App from "metabase/App.jsx"; diff --git a/frontend/src/metabase/store.js b/frontend/src/metabase/store.js index fe73cdfc1d9b1fd7649ff8f5dcade1ba5a36ba62..bc8ac82d1912df9a304a51ac9be57ac45bb1d89d 100644 --- a/frontend/src/metabase/store.js +++ b/frontend/src/metabase/store.js @@ -1,3 +1,5 @@ +/* @flow weak */ + import { combineReducers, applyMiddleware, createStore, compose } from 'redux' import { reducer as form } from "redux-form"; import { routerReducer as routing, routerMiddleware } from 'react-router-redux' diff --git a/frontend/src/metabase/user/actions.js b/frontend/src/metabase/user/actions.js index ea2227b4741fc9924a60edd84f5474567da0df86..6c01c55d570be2ba888c7c3ff3193e7dff7e8794 100644 --- a/frontend/src/metabase/user/actions.js +++ b/frontend/src/metabase/user/actions.js @@ -4,7 +4,7 @@ import { createThunkAction } from "metabase/lib/redux"; import { UserApi } from "metabase/services"; -import { refreshCurrentUser } from "metabase/user"; +import { refreshCurrentUser } from "metabase/redux/user"; // action constants export const CHANGE_TAB = 'CHANGE_TAB'; diff --git a/frontend/src/metabase/visualizations/AreaChart.jsx b/frontend/src/metabase/visualizations/AreaChart.jsx index c9c08e1ebf74eea07f0caaef9cce8bd45062f2f2..b53f4c8cd046d412ec3be0adabb6e2780fba148e 100644 --- a/frontend/src/metabase/visualizations/AreaChart.jsx +++ b/frontend/src/metabase/visualizations/AreaChart.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import LineAreaBarChart from "./components/LineAreaBarChart.jsx"; diff --git a/frontend/src/metabase/visualizations/BarChart.jsx b/frontend/src/metabase/visualizations/BarChart.jsx index f267dc73b80e60a685e16bce392d9c99ede36a74..1a546a5c6be8ea235a5339e1331c0abeca9ebe94 100644 --- a/frontend/src/metabase/visualizations/BarChart.jsx +++ b/frontend/src/metabase/visualizations/BarChart.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import LineAreaBarChart from "./components/LineAreaBarChart.jsx"; diff --git a/frontend/src/metabase/visualizations/Funnel.jsx b/frontend/src/metabase/visualizations/Funnel.jsx index 7dcbbd2e1a263a0a838d65446964b1db0fb2437b..f3ac709b6de50912f9dfbabd6045e5c27f27abcd 100644 --- a/frontend/src/metabase/visualizations/Funnel.jsx +++ b/frontend/src/metabase/visualizations/Funnel.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import ReactDOM from "react-dom"; @@ -7,7 +9,9 @@ import { formatValue } from "metabase/lib/formatting"; import { getSettings } from "metabase/lib/visualization_settings"; import i from "icepick"; -export default class Funnel extends Component { +import type { VisualizationProps } from "metabase/visualizations"; + +export default class Funnel extends Component<*, VisualizationProps, *> { static displayName = "Funnel"; static identifier = "funnel"; static iconName = "funnel"; diff --git a/frontend/src/metabase/visualizations/LineChart.jsx b/frontend/src/metabase/visualizations/LineChart.jsx index 45406129e96378c1a65c7b14db0eb5436199d53b..98d42bff66049d9688366d6c9f029206868e602a 100644 --- a/frontend/src/metabase/visualizations/LineChart.jsx +++ b/frontend/src/metabase/visualizations/LineChart.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import LineAreaBarChart from "./components/LineAreaBarChart.jsx"; diff --git a/frontend/src/metabase/visualizations/Map.jsx b/frontend/src/metabase/visualizations/Map.jsx index 1fa4d557f498a03856c3435494f7e9e1ae77df32..2c6880d7473d4e74dfb204b9f18dc7d068525221 100644 --- a/frontend/src/metabase/visualizations/Map.jsx +++ b/frontend/src/metabase/visualizations/Map.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import ChoroplethMap from "./components/ChoroplethMap.jsx"; @@ -5,7 +7,9 @@ import PinMap from "./PinMap.jsx"; import { ChartSettingsError } from "metabase/visualizations/lib/errors"; -export default class Map extends Component { +import type { VisualizationProps } from "metabase/visualizations"; + +export default class Map extends Component<*, VisualizationProps, *> { static displayName = "Map"; static identifier = "map"; static iconName = "pinmap"; diff --git a/frontend/src/metabase/visualizations/PieChart.jsx b/frontend/src/metabase/visualizations/PieChart.jsx index 4490dc65694a03c06ddea37dc3844e9a04b79974..53f47e1ec94fd515ef9d4ced058d0eee65da4e54 100644 --- a/frontend/src/metabase/visualizations/PieChart.jsx +++ b/frontend/src/metabase/visualizations/PieChart.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import ReactDOM from "react-dom"; import styles from "./PieChart.css"; @@ -26,7 +28,11 @@ const OTHER_SLICE_MIN_PERCENTAGE = 0.003; const PERCENT_REGEX = /percent/i; -export default class PieChart extends Component { +import type { VisualizationProps } from "metabase/visualizations"; + +type Props = VisualizationProps; + +export default class PieChart extends Component<*, Props, *> { static displayName = "Pie"; static identifier = "pie"; static iconName = "pie"; @@ -66,7 +72,8 @@ export default class PieChart extends Component { const showPercentInTooltip = !PERCENT_REGEX.test(cols[metricIndex].name) && !PERCENT_REGEX.test(cols[metricIndex].display_name); - let total = rows.reduce((sum, row) => sum + row[metricIndex], 0); + // $FlowFixMe + let total: number = 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(rows.length > 5 ? colors.harmony : colors.normal); diff --git a/frontend/src/metabase/visualizations/PinMap.jsx b/frontend/src/metabase/visualizations/PinMap.jsx index a43c7025d1d211c2794ba96a2e9db8f8c98ba021..e80bb7846462b54a8b6c079dbc067b7d888cf144 100644 --- a/frontend/src/metabase/visualizations/PinMap.jsx +++ b/frontend/src/metabase/visualizations/PinMap.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import { hasLatitudeAndLongitudeColumns } from "metabase/lib/schema_metadata"; @@ -11,12 +13,24 @@ import cx from "classnames"; import L from "leaflet"; +import type { VisualizationProps } from "metabase/visualizations"; + +type Props = VisualizationProps; + +type State = { + lat: ?number, + lng: ?number, + zoom: ?number, + points: L.Point[], + bounds: L.Bounds, +}; + const MAP_COMPONENTS_BY_TYPE = { "markers": LeafletMarkerPinMap, "tiles": LeafletTilePinMap, } -export default class PinMap extends Component { +export default class PinMap extends Component<*, Props, State> { static displayName = "Pin Map"; static identifier = "pin_map"; static iconName = "pinmap"; @@ -29,24 +43,25 @@ export default class PinMap extends Component { if (!hasLatitudeAndLongitudeColumns(cols)) { throw new LatitudeLongitudeError(); } } - constructor(props, context) { - super(props, context); + state: State; + + constructor(props: Props) { + super(props); this.state = { lat: null, lng: null, zoom: null, ...this._getPoints(props) }; - _.bindAll(this, "onMapZoomChange", "onMapCenterChange", "updateSettings"); } - componentWillReceiveProps(newProps) { + componentWillReceiveProps(newProps: Props) { if (newProps.series[0].data !== this.props.series[0].data) { this.setState(this._getPoints(newProps)) } } - updateSettings() { + updateSettings = () => { let newSettings = {}; if (this.state.lat != null) { newSettings["map.center_latitude"] = this.state.lat; @@ -61,15 +76,15 @@ export default class PinMap extends Component { this.setState({ lat: null, lng: null, zoom: null }); } - onMapCenterChange = (lat, lng) => { + onMapCenterChange = (lat: number, lng: number) => { this.setState({ lat, lng }); } - onMapZoomChange = (zoom) => { + onMapZoomChange = (zoom: number) => { this.setState({ zoom }); } - _getPoints(props) { + _getPoints(props: Props) { const { settings, series: [{ data: { cols, rows }}] } = props; const latitudeIndex = _.findIndex(cols, (col) => col.name === settings["map.latitude_column"]); const longitudeIndex = _.findIndex(cols, (col) => col.name === settings["map.longitude_column"]); @@ -91,7 +106,7 @@ export default class PinMap extends Component { const { points, bounds } = this.state;//this._getPoints(this.props); return ( - <div className={className + " PinMap relative"} onMouseDownCapture={(e) =>e.stopPropagation() /* prevent dragging */}> + <div className={cx(className, "PinMap relative")} onMouseDownCapture={(e) =>e.stopPropagation() /* prevent dragging */}> { Map ? <Map {...this.props} diff --git a/frontend/src/metabase/visualizations/Progress.jsx b/frontend/src/metabase/visualizations/Progress.jsx index faf2bd64d48e4eaa0b1632b54072c80e10045c05..abbaaf072af58bee35fc10f9dce4f75a295ca5a4 100644 --- a/frontend/src/metabase/visualizations/Progress.jsx +++ b/frontend/src/metabase/visualizations/Progress.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import ReactDOM from "react-dom"; @@ -10,7 +12,9 @@ import Color from "color"; const BORDER_RADIUS = 5; -export default class Progress extends Component { +import type { VisualizationProps } from "metabase/visualizations"; + +export default class Progress extends Component<*, VisualizationProps, *> { static displayName = "Progress"; static identifier = "progress"; static iconName = "progress"; @@ -76,7 +80,7 @@ export default class Progress extends Component { render() { const { series: [{ data: { rows } }], settings } = this.props; - const value = rows[0][0]; + const value: number = rows[0][0]; const goal = settings["progress.goal"] || 0; const mainColor = settings["progress.color"]; diff --git a/frontend/src/metabase/visualizations/Scalar.jsx b/frontend/src/metabase/visualizations/Scalar.jsx index 3b08089c20d4eb17c3b59efe753a31cedf55e63b..5feb602986717371999eb5abedfe876f9243fb3e 100644 --- a/frontend/src/metabase/visualizations/Scalar.jsx +++ b/frontend/src/metabase/visualizations/Scalar.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import { Link } from "react-router"; import styles from "./Scalar.css"; @@ -13,7 +15,9 @@ import cx from "classnames"; import i from "icepick"; import d3 from "d3"; -export default class Scalar extends Component { +import type { VisualizationProps } from "metabase/visualizations"; + +export default class Scalar extends Component<*, VisualizationProps, *> { static displayName = "Number"; static identifier = "scalar"; static iconName = "number"; diff --git a/frontend/src/metabase/visualizations/ScatterPlot.jsx b/frontend/src/metabase/visualizations/ScatterPlot.jsx index 0f5af3c5857028dcf750249f9dba71e4d689cbdd..f6ea82d56ac58dbb711bc6aab29785a6002a7e2b 100644 --- a/frontend/src/metabase/visualizations/ScatterPlot.jsx +++ b/frontend/src/metabase/visualizations/ScatterPlot.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import LineAreaBarChart from "./components/LineAreaBarChart.jsx"; diff --git a/frontend/src/metabase/visualizations/Table.jsx b/frontend/src/metabase/visualizations/Table.jsx index fa9c2708f758be12725500f054505f4fd2ef691a..8eb8be0ed7c4a9b0b582fc3d2fc915de56f1f72e 100644 --- a/frontend/src/metabase/visualizations/Table.jsx +++ b/frontend/src/metabase/visualizations/Table.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import TableInteractive from "./TableInteractive.jsx"; @@ -6,7 +8,26 @@ import TableSimple from "./TableSimple.jsx"; import * as DataGrid from "metabase/lib/data_grid"; import _ from "underscore"; -export default class Bar extends Component { +import type { DatasetData } from "metabase/meta/types/Dataset"; +import type { Card, VisualizationSettings } from "metabase/meta/types/Card"; + +type Props = { + card: Card, + data: DatasetData, + settings: VisualizationSettings, + isDashboard: boolean, + cellClickedFn: (number, number) => void, + cellIsClickableFn: (number, number) => boolean, + setSortFn: (/* TODO */) => void, +} +type State = { + data: ?DatasetData, + columnIndexes: number[] +} + +export default class Bar extends Component<*, Props, State> { + state: State; + static displayName = "Table"; static identifier = "table"; static iconName = "table"; @@ -21,11 +42,12 @@ export default class Bar extends Component { // scalar can always be rendered, nothing needed here } - constructor(props, context) { - super(props, context); + constructor(props: Props) { + super(props); this.state = { - data: null + data: null, + columnIndexes: [] }; } @@ -33,22 +55,22 @@ export default class Bar extends Component { this._updateData(this.props); } - componentWillReceiveProps(newProps) { + componentWillReceiveProps(newProps: Props) { // TODO: remove use of deprecated "card" and "data" props if (newProps.data !== this.props.data || !_.isEqual(newProps.settings, this.props.settings)) { this._updateData(newProps); } } - cellClicked = (rowIndex, columnIndex, ...args) => { + cellClicked = (rowIndex: number, columnIndex: number, ...args: any[]) => { this.props.cellClickedFn(rowIndex, this.state.columnIndexes[columnIndex], ...args); } - cellIsClickable = (rowIndex, columnIndex, ...args) => { + cellIsClickable = (rowIndex: number, columnIndex: number, ...args: any[]) => { return this.props.cellIsClickableFn(rowIndex, this.state.columnIndexes[columnIndex], ...args); } - _updateData({ data, settings }) { + _updateData({ data, settings }: { data: DatasetData, settings: VisualizationSettings }) { if (settings["table.pivot"]) { this.setState({ data: DataGrid.pivot(data) diff --git a/frontend/src/metabase/visualizations/TableInteractive.jsx b/frontend/src/metabase/visualizations/TableInteractive.jsx index 285afc8fec1f63d9dddad7c145b972622fca79e4..43884f285a214e58c345c91054316ec47be79800 100644 --- a/frontend/src/metabase/visualizations/TableInteractive.jsx +++ b/frontend/src/metabase/visualizations/TableInteractive.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import ReactDOM from "react-dom"; @@ -24,10 +26,47 @@ const ROW_HEIGHT = 35; const MIN_COLUMN_WIDTH = ROW_HEIGHT; const RESIZE_HANDLE_WIDTH = 5; +import type { Column } from "metabase/meta/types/Dataset"; +import type { VisualizationProps } from "metabase/visualizations"; + +type Props = VisualizationProps & { + width: number, + height: number, + sort: any, + isPivoted: boolean, + cellClickedFn: (number, number) => void, + cellIsClickableFn: (number, number) => boolean, + setSortFn: (/* TODO */) => void, +} +type State = { + popover: ?{ rowIndex: number, columnIndex: number }, + columnWidths: number[], + contentWidths: ?number[] +} + +type CellRendererProps = { + key: string, + style: { [key:string]: any }, + columnIndex: number, + rowIndex: number +} + +type GridComponent = Component<void, void, void> & { recomputeGridSize: () => void } + @ExplicitSize -export default class TableInteractive extends Component { - constructor(props, context) { - super(props, context); +export default class TableInteractive extends Component<*, Props, State> { + state: State; + props: Props; + + columnHasResized: { [key:number]: boolean }; + columnNeedsResize: { [key:number]: boolean }; + _div: HTMLElement; + + header: GridComponent; + grid: GridComponent; + + constructor(props: Props) { + super(props); this.state = { popover: null, @@ -35,8 +74,6 @@ export default class TableInteractive extends Component { contentWidths: null }; this.columnHasResized = {}; - - _.bindAll(this, "onClosePopover", "cellRenderer", "tableHeaderRenderer"); } static propTypes = { @@ -61,29 +98,29 @@ export default class TableInteractive extends Component { this._div.style.display = "inline-block" this._div.style.position = "absolute" this._div.style.visibility = "hidden" - this._div.style.zIndex = -1 + this._div.style.zIndex = "-1" document.body.appendChild(this._div); this._measure(); } componentWillUnmount() { - if (this._div) { + if (this._div && this._div.parentNode) { this._div.parentNode.removeChild(this._div); } } - componentWillReceiveProps(newProps) { + componentWillReceiveProps(newProps: Props) { if (JSON.stringify(this.props.data && this.props.data.cols) !== JSON.stringify(newProps.data && newProps.data.cols)) { this.resetColumnWidths(); } } - shouldComponentUpdate(nextProps, nextState) { - const PROP_KEYS = ["width", "height", "settings", "data"]; + shouldComponentUpdate(nextProps: Props, nextState: State) { + const PROP_KEYS: string[] = ["width", "height", "settings", "data"]; // compare specific props and state to determine if we should re-render return ( - !_.isEqual(_.pick(this.props, PROP_KEYS), _.pick(nextProps, PROP_KEYS)) || + !_.isEqual(_.pick(this.props, ...PROP_KEYS), _.pick(nextProps, ...PROP_KEYS)) || !_.isEqual(this.state, nextState) ); } @@ -110,7 +147,7 @@ export default class TableInteractive extends Component { this._measureColumn(index) ); - let columnWidths = cols.map((col, index) => { + let columnWidths: number[] = cols.map((col, index) => { if (this.columnNeedsResize) { if (this.columnNeedsResize[index] && !this.columnHasResized[index]) { this.columnHasResized[index] = true; @@ -128,18 +165,18 @@ export default class TableInteractive extends Component { this.setState({ contentWidths, columnWidths }, this.recomputeGridSize); } - _measureColumn(columnIndex) { + _measureColumn(columnIndex: number) { const { data: { rows } } = this.props; let width = MIN_COLUMN_WIDTH; // measure column header - width = Math.max(width, this._measureCell(this.tableHeaderRenderer({ columnIndex }))); + width = Math.max(width, this._measureCell(this.tableHeaderRenderer({ columnIndex, rowIndex: 0, key: "", style: {} }))); // measure up to 10 non-nil cells let remaining = 10; for (let rowIndex = 0; rowIndex < rows.length && remaining > 0; rowIndex++) { if (rows[rowIndex][columnIndex] != null) { - const cellWidth = this._measureCell(this.cellRenderer({ rowIndex, columnIndex })); + const cellWidth = this._measureCell(this.cellRenderer({ rowIndex, columnIndex, key: "", style: {} })); width = Math.max(width, cellWidth); remaining--; } @@ -148,7 +185,7 @@ export default class TableInteractive extends Component { return width; } - _measureCell(cell) { + _measureCell(cell: React.Element<any>) { ReactDOM.unstable_renderSubtreeIntoContainer(this, cell, this._div); // 2px for border? @@ -170,13 +207,13 @@ export default class TableInteractive extends Component { this.setState({ contentWidths: null }) }, 100) - onCellResize(columnIndex) { + onCellResize(columnIndex: number) { this.columnNeedsResize = this.columnNeedsResize || {} this.columnNeedsResize[columnIndex] = true; this.recomputeColumnSizes(); } - onColumnResize(columnIndex, width) { + onColumnResize(columnIndex: number, width: number) { const { settings } = this.props; let columnWidthsSetting = settings["table.column_widths"] ? settings["table.column_widths"].slice() : []; columnWidthsSetting[columnIndex] = Math.max(MIN_COLUMN_WIDTH, width); @@ -188,22 +225,22 @@ export default class TableInteractive extends Component { return (this.props.setSortFn !== undefined); } - setSort(column) { + setSort(column: Column) { // lets completely delegate this to someone else up the stack :) this.props.setSortFn(column); MetabaseAnalytics.trackEvent('QueryBuilder', 'Set Sort', 'table column'); } - cellClicked(rowIndex, columnIndex) { + cellClicked(rowIndex: number, columnIndex: number) { this.props.cellClickedFn(rowIndex, columnIndex); } - popoverFilterClicked(rowIndex, columnIndex, operator) { + popoverFilterClicked(rowIndex: number, columnIndex: number, operator: string) { this.props.cellClickedFn(rowIndex, columnIndex, operator); this.setState({ popover: null }); } - showPopover(rowIndex, columnIndex) { + showPopover(rowIndex: number, columnIndex: number) { this.setState({ popover: { rowIndex: rowIndex, @@ -212,11 +249,11 @@ export default class TableInteractive extends Component { }); } - onClosePopover() { + onClosePopover = () => { this.setState({ popover: null }); } - cellRenderer({ key, style, rowIndex, columnIndex }) { + cellRenderer = ({ key, style, rowIndex, columnIndex }: CellRendererProps) => { const { data: { cols, rows }} = this.props; const column = cols[columnIndex]; const cellData = rows[rowIndex][columnIndex]; @@ -247,7 +284,7 @@ export default class TableInteractive extends Component { <Value value={cellData} column={column} onResize={this.onCellResize.bind(this, columnIndex)} /> { popover && popover.rowIndex === rowIndex && popover.columnIndex === columnIndex && <QuickFilterPopover - column={cols[this.state.popover.columnIndex]} + column={cols[popover.columnIndex]} onFilter={this.popoverFilterClicked.bind(this, rowIndex, columnIndex)} onClose={this.onClosePopover} /> @@ -257,7 +294,7 @@ export default class TableInteractive extends Component { } } - tableHeaderRenderer({ key, style, columnIndex }) { + tableHeaderRenderer = ({ key, style, columnIndex }: CellRendererProps) => { const { sort, data: { cols }} = this.props; const isSortable = this.isSortable(); const column = cols[columnIndex]; @@ -309,7 +346,7 @@ export default class TableInteractive extends Component { ) } - getColumnWidth = ({ index }) => { + getColumnWidth = ({ index }: { index: number }) => { const { settings } = this.props; const { columnWidths } = this.state; const columnWidthsSetting = settings["table.column_widths"] || []; diff --git a/frontend/src/metabase/visualizations/TableSimple.jsx b/frontend/src/metabase/visualizations/TableSimple.jsx index cb26c420fb679d52b905222d47f4ae814d48331c..9c919ba001f388d0b82cf9150f22778d7011425d 100644 --- a/frontend/src/metabase/visualizations/TableSimple.jsx +++ b/frontend/src/metabase/visualizations/TableSimple.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import ReactDOM from "react-dom"; import styles from "./Table.css"; @@ -12,10 +14,26 @@ import { getFriendlyName } from "metabase/visualizations/lib/utils"; import cx from "classnames"; import _ from "underscore"; +import type { VisualizationProps } from "metabase/visualizations"; + +type Props = VisualizationProps & { + height: number, + className?: string +} + +type State = { + page: number, + pageSize: number, + sortColumn: ?number, + sortDescending: boolean +} + @ExplicitSize -export default class TableSimple extends Component { - constructor(props, context) { - super(props, context); +export default class TableSimple extends Component<*, Props, State> { + state: State; + + constructor(props: Props) { + super(props); this.state = { page: 0, @@ -33,7 +51,7 @@ export default class TableSimple extends Component { className: "" }; - setSort(colIndex) { + setSort(colIndex: number) { if (this.state.sortColumn === colIndex) { this.setState({ sortDescending: !this.state.sortDescending }); } else { diff --git a/frontend/src/metabase/visualizations/XKCDChart.css b/frontend/src/metabase/visualizations/XKCDChart.css deleted file mode 100644 index 7d12a325ef668e25df4b1d55262c498f13d8d48f..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/visualizations/XKCDChart.css +++ /dev/null @@ -1,27 +0,0 @@ - -:local(.XKCDChart) { - font-family: "Humor Sans", sans-serif; - font-size: 16px; - color: #333; - text-align: center; -} - -:local(.XKCDChart) text.title { - font-size: 20px; -} - -:local(.XKCDChart) path { - fill: none; - stroke-width: 2.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - -:local(.XKCDChart) path.axis { - stroke: black; -} - -:local(.XKCDChart) path.bgline { - stroke: white; - stroke-width: 6px; -} diff --git a/frontend/src/metabase/visualizations/XKCDChart.jsx b/frontend/src/metabase/visualizations/XKCDChart.jsx deleted file mode 100644 index a8a61456dc9380662155493385fc9623dd242942..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/visualizations/XKCDChart.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { Component, PropTypes } from "react"; -import ReactDOM from "react-dom"; - -import styles from "./XKCDChart.css"; - -import { MinColumnsError, MinRowsError } from "metabase/visualizations/lib/errors"; - -import { - getFriendlyName, - getAvailableCanvasWidth, - getAvailableCanvasHeight -} from "metabase/visualizations/lib/utils"; - -import { dimensionIsTimeseries } from "./lib/timeseries"; - -import xkcdplot from "xkcdplot"; -import "xkcdplot/humor-sans"; - -import cx from "classnames"; - -export default class XKCDChart extends Component { - static displayName = "XKCD" - static identifier = "xkcd"; - static iconName = "pinmap"; - - static noHeader = true; - - static isSensible(cols, rows) { - return rows.length > 1 && cols.length > 1; - } - - static checkRenderable(cols, rows) { - if (cols.length < 2) { throw new MinColumnsError(2, cols.length); } - if (rows.length < 1) { throw new MinRowsError(1, rows.length); } - } - - componentDidMount() { - this.componentDidUpdate(); - } - - componentDidUpdate() { - let { series } = this.props; - - let parent = ReactDOM.findDOMNode(this); - while (parent.firstChild) { - parent.removeChild(parent.firstChild); - } - - let margin = 50; - - // Build the plot. - var plot = xkcdplot() - .width(Math.min(800, getAvailableCanvasWidth(parent) - margin * 2)) - .height(Math.min(600, getAvailableCanvasHeight(parent) - margin * 2)) - .margin(margin) - .xlabel(getFriendlyName(series[0].data.cols[0])) - .ylabel(getFriendlyName(series[0].data.cols[1])); - - if (series[0].card.name) { - plot.title(series[0].card.name); - } - - plot(parent); - - let colors = [undefined, "red", "grey", "green", "yellow"]; - let isTimeseries = dimensionIsTimeseries(series[0].data); - series.map((s, index) => { - let data = s.data.rows.map(row => ({ - x: isTimeseries ? new Date(row[0]).getTime() : row[0], - y: row[1] - })); - plot.plot(data, { stroke: colors[index % colors.length] }); - }) - - plot.draw(); - - if (series[0].card.id) { - let title = parent.querySelector(".title"); - title.style = "cursor: pointer;"; - title.addEventListener("click", () => window.location = "/card/" + series[0].card.id); - } - } - - render() { - return ( - <div className={cx(this.props.className, styles.XKCDChart)} /> - ); - } -} diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx index 7022b90c84e79ee04e36399c6ed55ade3d057115..3abcb36840acfb78e0ec31115323bba8147813c2 100644 --- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx +++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx @@ -1,3 +1,5 @@ +/* @flow */ + import React, { Component, PropTypes } from "react"; import CardRenderer from "./CardRenderer.jsx"; @@ -20,7 +22,11 @@ import crossfilter from "crossfilter"; import _ from "underscore"; import cx from "classnames"; -export default class LineAreaBarChart extends Component { +import type { VisualizationProps } from "metabase/visualizations"; + +export default class LineAreaBarChart extends Component<*, VisualizationProps, *> { + static identifier; + static noHeader = true; static supportsSeries = true; @@ -81,7 +87,6 @@ export default class LineAreaBarChart extends Component { static propTypes = { series: PropTypes.array.isRequired, - onAddSeries: PropTypes.func, actionButtons: PropTypes.node, isDashboard: PropTypes.bool }; diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx index c66217a0826d86e9e9278ad13479ac943318548c..e464edc263133a1eb4cd15e102ad06856075fa9e 100644 --- a/frontend/src/metabase/visualizations/components/Visualization.jsx +++ b/frontend/src/metabase/visualizations/components/Visualization.jsx @@ -1,6 +1,6 @@ -/* eslint "react/prop-types": "warn" */ +/* @flow weak */ -import React, { Component, PropTypes } from "react"; +import React, { Component, PropTypes, Element } from "react"; import ExplicitSize from "metabase/components/ExplicitSize.jsx"; import LegendHeader from "metabase/visualizations/components/LegendHeader.jsx"; @@ -24,58 +24,77 @@ import cx from "classnames"; export const ERROR_MESSAGE_GENERIC = "There was a problem displaying this chart."; export const ERROR_MESSAGE_PERMISSION = "Sorry, you don't have permission to see this card." -@ExplicitSize -export default class Visualization extends Component { - constructor(props, context) { - super(props, context) +import type { VisualizationSettings } from "metabase/meta/types/Card"; +import type { HoverObject, Series } from "metabase/visualizations"; - this.state = { - hovered: null, - error: null, - warnings: [], - yAxisSplit: null, - }; +type Props = { + series: Series, - _.bindAll(this, "onRender", "onRenderError", "onHoverChange"); - } + className: string, - static propTypes = { - series: PropTypes.array.isRequired, + isDashboard: boolean, + isEditing: boolean, - className: PropTypes.string, + actionButtons: Element<any>, - isDashboard: PropTypes.bool, - isEditing: PropTypes.bool, + // errors + error: string, + errorIcon: string, - actionButtons: PropTypes.node, + // slow card warnings + isSlow: boolean, + expectedDuration: number, - // errors - error: PropTypes.string, - errorIcon: PropTypes.string, + // injected by ExplicitSize + width: number, + height: number, - // slow card warnings - isSlow: PropTypes.bool, - expectedDuration: PropTypes.number, + // settings overrides from settings panel + settings: VisualizationSettings, - // injected by ExplicitSize - width: PropTypes.number, - height: PropTypes.number, + // used for showing content in place of visualization, e.x. dashcard filter mapping + replacementContent: Element<any>, - // settings overrides from settings panel - settings: PropTypes.object, + // used by TableInteractive + setSortFn: (any) => void, + cellIsClickableFn: (number, number) => boolean, + cellClickedFn: (number, number) => void, - // used for showing content in place of visualization, e.x. dashcard filter mapping - replacementContent: PropTypes.node, + // misc + onUpdateWarnings: (string[]) => void, + onOpenChartSettings: () => void, +} - // used by TableInteractive - setSortFn: PropTypes.func, - cellIsClickableFn: PropTypes.func, - cellClickedFn: PropTypes.func, +type State = { + series: ?Series, + CardVisualization: ?(Component<*, VisualizationSettings, *> & { + checkRenderable: (any, any) => void, + noHeader: boolean + }), + + hovered: ?HoverObject, + error: ?Error, + warnings: string[], + yAxisSplit: ?number[][], +} - // misc - onUpdateWarnings: PropTypes.func, - onOpenChartSettings: PropTypes.func, - }; +@ExplicitSize +export default class Visualization extends Component<*, Props, State> { + state: State; + props: Props; + + constructor(props: Props) { + super(props); + + this.state = { + hovered: null, + error: null, + warnings: [], + yAxisSplit: null, + series: null, + CardVisualization: null + }; + } static defaultProps = { isDashboard: false, @@ -103,6 +122,7 @@ export default class Visualization extends Component { } } + // $FlowFixMe getWarnings(props = this.props, state = this.state) { let warnings = state.warnings || []; // don't warn about truncated data for table since we show a warning in the row count @@ -128,7 +148,7 @@ export default class Visualization extends Component { }); } - onHoverChange(hovered) { + onHoverChange = (hovered) => { const { yAxisSplit } = this.state; if (hovered) { // if we have Y axis split info then find the Y axis index (0 = left, 1 = right) @@ -140,11 +160,11 @@ export default class Visualization extends Component { this.setState({ hovered }); } - onRender({ yAxisSplit, warnings = [] } = {}) { + onRender = ({ yAxisSplit, warnings = [] } = {}) => { this.setState({ yAxisSplit, warnings }); } - onRenderError(error) { + onRenderError = (error) => { this.setState({ error }) } @@ -154,7 +174,7 @@ export default class Visualization extends Component { const small = width < 330; let error = this.props.error || this.state.error; - let loading = !(series.length > 0 && _.every(series, (s) => s.data)); + let loading = !(series && series.length > 0 && _.every(series, (s) => s.data)); let noResults = false; // don't try to load settings unless data is loaded @@ -167,6 +187,7 @@ export default class Visualization extends Component { } else { try { if (CardVisualization.checkRenderable) { + // $FlowFixMe CardVisualization.checkRenderable(series[0].data.cols, series[0].data.rows, settings); } } catch (e) { @@ -209,12 +230,13 @@ export default class Visualization extends Component { return ( <div className={cx(className, "flex flex-column")}> - { isDashboard && (settings["card.title"] || extra) && (loading || error || !CardVisualization.noHeader) || replacementContent ? + { isDashboard && (settings["card.title"] || extra) && (loading || error || !(CardVisualization && CardVisualization.noHeader)) || replacementContent ? <div className="p1 flex-no-shrink"> <LegendHeader series={ settings["card.title"] ? // if we have a card title set, use it + // $FlowFixMe setIn(series, [0, "card", "name"], settings["card.title"]) : // otherwise use the original series series @@ -272,12 +294,15 @@ export default class Visualization extends Component { } </div> : + // $FlowFixMe <CardVisualization {...this.props} className="flex-full" series={series} settings={settings} + // $FlowFixMe card={series[0].card} // convienence for single-series visualizations + // $FlowFixMe data={series[0].data} // convienence for single-series visualizations hovered={this.state.hovered} onHoverChange={this.onHoverChange} diff --git a/frontend/src/metabase/visualizations/index.js b/frontend/src/metabase/visualizations/index.js index 1e3b9937064dcb62e33125511578ae3855427121..a6ed3c41f2847c012e1b852ff615130743db227a 100644 --- a/frontend/src/metabase/visualizations/index.js +++ b/frontend/src/metabase/visualizations/index.js @@ -1,3 +1,6 @@ +/* @flow weak */ + +import { Component } from "react"; import Scalar from "./Scalar.jsx"; import Progress from "./Progress.jsx"; @@ -12,8 +15,44 @@ import Funnel from "./Funnel.jsx"; import _ from "underscore"; +import type { DatasetData } from "metabase/meta/types/Dataset"; +import type { Card, VisualizationSettings } from "metabase/meta/types/Card"; + +export type HoverObject = { + index?: number, + axisIndex?: number +} + +// type Visualization = Component<*, VisualizationProps, *>; + +// $FlowFixMe +export type Series = { card: Card, data: DatasetData }[] & { _raw: Series } + +export type VisualizationProps = { + series: Series, + card: Card, + data: DatasetData, + settings: VisualizationSettings, + + className?: string, + gridSize: ?{ + width: number, + height: number + }, + + isDashboard: boolean, + isEditing: boolean, + actionButtons: Node, + + hovered: ?HoverObject, + onHoverChange: (?HoverObject) => void, + + onUpdateVisualizationSettings: ({ [key: string]: any }) => void +} + const visualizations = new Map(); const aliases = new Map(); +// $FlowFixMe visualizations.get = function(key) { return Map.prototype.get.call(this, key) || aliases.get(key) || Table; } @@ -32,14 +71,14 @@ export function registerVisualization(visualization) { } } -export function getVisualizationRaw(series) { +export function getVisualizationRaw(series: Series) { return { series: series, CardVisualization: visualizations.get(series[0].card.display) }; } -export function getVisualizationTransformed(series) { +export function getVisualizationTransformed(series: Series) { // don't transform if we don't have the data if (_.any(series, s => s.data == null)) { return getVisualizationRaw(series); @@ -49,12 +88,16 @@ export function getVisualizationTransformed(series) { let CardVisualization, lastSeries; do { CardVisualization = visualizations.get(series[0].card.display); + if (!CardVisualization) { + throw new Error("No visualization for " + series[0].card.display); + } lastSeries = series; if (typeof CardVisualization.transformSeries === "function") { series = CardVisualization.transformSeries(series); } if (series !== lastSeries) { series = [...series]; + // $FlowFixMe series._raw = lastSeries; } } while (series !== lastSeries); @@ -73,9 +116,4 @@ registerVisualization(PieChart); registerVisualization(MapViz); registerVisualization(Funnel); -import { enableVisualizationEasterEgg } from "./lib/utils"; -import XKCDChart from "./XKCDChart.jsx"; -import LineAreaBarChart from "./components/LineAreaBarChart.jsx"; -enableVisualizationEasterEgg("XKCD", LineAreaBarChart, XKCDChart); - export default visualizations; diff --git a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js index ff4f99e33ee3e01f0499f4fc8ac7b4709ee45316..a72d304ce1d53e2b6deeeb1af9b5f836ceab8819 100644 --- a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js +++ b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js @@ -1,3 +1,5 @@ +/* @flow weak */ + import crossfilter from "crossfilter"; import d3 from "d3"; import dc from "dc"; @@ -47,11 +49,11 @@ const VORONOI_MAX_POINTS = 300; const UNAGGREGATED_DATA_WARNING = (col) => `"${getFriendlyName(col)}" is an unaggregated field: if it has more than one value at a point on the x-axis, the values will be summed.` const NULL_DIMENSION_WARNING = "Data includes missing dimension values."; -function adjustTicksIfNeeded(axis, axisSize, minPixelsPerTick) { - let numTicks = axis.ticks(); +function adjustTicksIfNeeded(axis, axisSize: number, minPixelsPerTick: number) { + const ticks = axis.ticks(); // d3.js is dumb and sometimes numTicks is a number like 10 and other times it is an Array like [10] // if it's an array then convert to a num - numTicks = numTicks.length != null ? numTicks[0] : numTicks; + const numTicks: number = Array.isArray(ticks) ? ticks[0] : ticks; if ((axisSize / numTicks) < minPixelsPerTick) { axis.ticks(Math.round(axisSize / minPixelsPerTick)); @@ -192,14 +194,14 @@ function applyChartOrdinalXAxis(chart, settings, series, xValues) { function applyChartYAxis(chart, settings, series, yExtent, axisName) { let axis; - if (axisName === "left") { + if (axisName !== "right") { axis = { scale: (...args) => chart.y(...args), axis: (...args) => chart.yAxis(...args), label: (...args) => chart.yAxisLabel(...args), setting: (name) => settings["graph.y_axis." + name] }; - } else if (axisName === "right") { + } else { axis = { scale: (...args) => chart.rightY(...args), axis: (...args) => chart.rightYAxis(...args), @@ -441,6 +443,7 @@ function lineAndBarOnRender(chart, settings, onGoalHover, isSplitAxis) { function dispatchUIEvent(element, eventName) { let e = document.createEvent("UIEvents"); + // $FlowFixMe e.initUIEvent(eventName, true, true, window, 1); element.dispatchEvent(e); } @@ -713,9 +716,11 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender, if (settings["line.missing"] === "zero" || settings["line.missing"] === "none") { if (isTimeseries) { + // $FlowFixMe + const { interval, count } = xInterval; // replace xValues with - xValues = d3.time[xInterval.interval] - .range(xDomain[0], moment(xDomain[1]).add(1, "ms"), xInterval.count) + xValues = d3.time[interval] + .range(xDomain[0], moment(xDomain[1]).add(1, "ms"), count) .map(d => moment(d)); datas = fillMissingValues( datas, diff --git a/frontend/src/metabase/visualizations/lib/errors.js b/frontend/src/metabase/visualizations/lib/errors.js index c7e5e7a46eaf86b9c04135d5d023659a587eb63b..22e3c462a7b0393b47ac0348ba21dd38e7bea6f7 100644 --- a/frontend/src/metabase/visualizations/lib/errors.js +++ b/frontend/src/metabase/visualizations/lib/errors.js @@ -1,29 +1,30 @@ +/* @flow */ import { inflect } from "metabase/lib/formatting"; -export class MinColumnsError { - constructor(minColumns, actualColumns) { - this.message = `Doh! The data from your query doesn't fit the chosen display choice. This visualization requires at least ${actualColumns} ${inflect("column", actualColumns)} of data.`; +export class MinColumnsError extends Error { + constructor(minColumns: number, actualColumns: number) { + super(`Doh! The data from your query doesn't fit the chosen display choice. This visualization requires at least ${actualColumns} ${inflect("column", actualColumns)} of data.`); } } -export class MinRowsError { - constructor(minRows, actualRows) { - this.message = `No dice. We have ${actualRows} data ${inflect("point", actualRows)} to show and that's not enough for this visualization.`; - this.minRows = minRows; - this.actualRows = actualRows; +export class MinRowsError extends Error { + constructor(minRows: number, actualRows: number) { + super(`No dice. We have ${actualRows} data ${inflect("point", actualRows)} to show and that's not enough for this visualization.`); } } -export class LatitudeLongitudeError { - constructor(minRows, actualRows) { - this.message = "Bummer. We can't actually do a pin map for this data because we require both a latitude and longitude column."; +export class LatitudeLongitudeError extends Error { + constructor() { + super("Bummer. We can't actually do a pin map for this data because we require both a latitude and longitude column."); } } -export class ChartSettingsError { - constructor(message, section, buttonText) { - this.message = message || "Please configure this chart in the chart settings"; +export class ChartSettingsError extends Error { + section: ?string; + buttonText: ?string; + constructor(message: string, section?: string, buttonText?: string) { + super(message || "Please configure this chart in the chart settings"); this.section = section; this.buttonText = buttonText || "Edit Settings"; } diff --git a/frontend/src/metabase/visualizations/lib/mapping.js b/frontend/src/metabase/visualizations/lib/mapping.js index 52d453bc6a9311013d82736dd4055892ed4fc897..d2f23e2f8a0e1cff80aa75b3c9669e5c70b84b9b 100644 --- a/frontend/src/metabase/visualizations/lib/mapping.js +++ b/frontend/src/metabase/visualizations/lib/mapping.js @@ -1,3 +1,4 @@ +/* @flow weak */ import L from "leaflet/dist/leaflet-src.js"; import d3 from "d3"; diff --git a/frontend/src/metabase/visualizations/lib/numeric.js b/frontend/src/metabase/visualizations/lib/numeric.js index bb671b949330e95de50d8b354b3ea793776874af..aa6de4a28a742bbce34d6fa5e39af37ff9e21136 100644 --- a/frontend/src/metabase/visualizations/lib/numeric.js +++ b/frontend/src/metabase/visualizations/lib/numeric.js @@ -1,3 +1,5 @@ +/* @flow weak */ + import { isNumeric } from "metabase/lib/schema_metadata"; export function dimensionIsNumeric({ cols, rows }, i = 0) { diff --git a/frontend/src/metabase/visualizations/lib/timeseries.js b/frontend/src/metabase/visualizations/lib/timeseries.js index e76c13ae91e8b1989157c01b5674b33d4838d5fd..9f0a31934013dd204c09eb3f209d506018dba1f4 100644 --- a/frontend/src/metabase/visualizations/lib/timeseries.js +++ b/frontend/src/metabase/visualizations/lib/timeseries.js @@ -1,3 +1,5 @@ +/* @flow weak */ + import moment from "moment"; import { isDate } from "metabase/lib/schema_metadata"; diff --git a/frontend/src/metabase/visualizations/lib/tooltip.js b/frontend/src/metabase/visualizations/lib/tooltip.js index 3d470b38420dd0b9b73212b02124f825c2f951f8..15d5d8a8a671e8b64678470e7ffaae7132984947 100644 --- a/frontend/src/metabase/visualizations/lib/tooltip.js +++ b/frontend/src/metabase/visualizations/lib/tooltip.js @@ -1,3 +1,5 @@ +/* @flow weak */ + function getElementIndex(e) { return [...e.classList].map(c => c.match(/^_(\d+)$/)).filter(c => c).map(c => parseInt(c[1], 10))[0]; } diff --git a/frontend/src/metabase/visualizations/lib/utils.js b/frontend/src/metabase/visualizations/lib/utils.js index 87e91dd554327ff3a14d44cde16536abee9df5fe..0080fff76bef2b8443ef908f116f46aae4e625d3 100644 --- a/frontend/src/metabase/visualizations/lib/utils.js +++ b/frontend/src/metabase/visualizations/lib/utils.js @@ -1,3 +1,4 @@ +/* @flow weak */ import React from "react"; import _ from "underscore"; @@ -74,7 +75,7 @@ export function computeSplit(extents) { bestCost = splitCost; } } - return best.sort((a,b) => a[0] - b[0]); + return best && best.sort((a,b) => a[0] - b[0]); } const FRIENDLY_NAME_MAP = { @@ -87,7 +88,7 @@ const FRIENDLY_NAME_MAP = { export function getXValues(datas, chartType) { let xValues = _.chain(datas) - .map((data) => _.pluck(data, 0)) + .map((data) => _.pluck(data, "0")) .flatten(true) .uniq() .value(); diff --git a/package.json b/package.json index f884c5654b91cc06e2f0e31782098c10dac2e67e..3b5f764220ae2778ea7bc4aa1991b877a4308e7a 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "stack-source-map": "^1.0.4", "tether": "^1.2.0", "underscore": "^1.8.3", - "xkcdplot": "^1.1.0", "z-index": "0.0.1" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index f728df13b3f93d6f8d2f85036f406fa6ede80f4d..c40f60caee784dcd153646f33662f9253dd14a6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2141,7 +2141,7 @@ cycle@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" -d3@^3, d3@^3.5.17, d3@^3.5.x: +d3@^3, d3@^3.5.17: version "3.5.17" resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" @@ -6950,12 +6950,6 @@ xdg-basedir@^2.0.0: dependencies: os-homedir "^1.0.0" -xkcdplot@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/xkcdplot/-/xkcdplot-1.1.0.tgz#de8a9ba73c0f86ec72818bda1b5433f78d364836" - dependencies: - d3 "^3.5.x" - xml-char-classes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/xml-char-classes/-/xml-char-classes-1.0.0.tgz#64657848a20ffc5df583a42ad8a277b4512bbc4d"