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

Extract table formatting code and use for pulses

parent e8de79d1
No related branches found
No related tags found
No related merge requests found
import "babel-polyfill";
import { makeCellBackgroundGetter } from "metabase/visualizations/lib/table_format";
global.console = {
log: print,
warn: print,
error: print,
};
global.makeCellBackgroundGetter = function(data, settings) {
data = JSON.parse(data);
settings = JSON.parse(settings);
try {
const getter = makeCellBackgroundGetter(data, settings);
return (value, rowIndex, colName) => {
const color = getter(value, rowIndex, colName);
if (color) {
return roundColor(color);
}
};
} catch (e) {
print("ERROR", e);
return () => null;
}
};
// HACK: d3 may return rgb values with decimals but the rendering engine used for pulses doesn't support that
function roundColor(color) {
return color.replace(
/rgba\((\d+(?:\.\d+)),\s*(\d+(?:\.\d+)),\s*(\d+(?:\.\d+)),\s*(\d+\.\d+)\)/,
(_, r, g, b, a) =>
`rgba(${Math.round(r)},${Math.round(g)},${Math.round(b)},${a})`,
);
}
import { alpha, getColorScale } from "metabase/lib/colors";
import _ from "underscore";
import d3 from "d3";
const CELL_ALPHA = 0.65;
const ROW_ALPHA = 0.2;
const GRADIENT_ALPHA = 0.75;
export function makeCellBackgroundGetter(data, settings) {
const { rows, cols } = data;
const formats = settings["table.column_formatting"];
const pivot = settings["table.pivot"];
let formatters = {};
let rowFormatters = [];
try {
const columnExtents = computeColumnExtents(formats, data);
formatters = compileFormatters(formats, columnExtents);
rowFormatters = compileRowFormatters(formats, columnExtents);
} catch (e) {
console.error(e);
}
const colIndexes = _.object(cols.map((col, index) => [col.name, index]));
if (Object.values(formatters).length === 0 && rowFormatters.length === 0) {
return () => null;
} else {
return function(value, rowIndex, colName) {
if (formatters[colName]) {
// const value = rows[rowIndex][colIndexes[colName]];
for (const formatter of formatters[colName]) {
const color = formatter(value);
if (color != null) {
return color;
}
}
}
// don't highlight row for pivoted tables
if (!pivot) {
for (const rowFormatter of rowFormatters) {
const color = rowFormatter(rows[rowIndex], colIndexes);
if (color != null) {
return color;
}
}
}
};
}
}
function compileFormatter(
format,
columnName,
columnExtents,
isRowFormatter = false,
) {
if (format.type === "single") {
let { operator, value, color } = format;
if (isRowFormatter) {
color = alpha(color, ROW_ALPHA);
} else {
color = alpha(color, CELL_ALPHA);
}
switch (operator) {
case "<":
return v => (v < value ? color : null);
case "<=":
return v => (v <= value ? color : null);
case ">=":
return v => (v >= value ? color : null);
case ">":
return v => (v > value ? color : null);
case "=":
return v => (v === value ? color : null);
case "!=":
return v => (v !== value ? color : null);
}
} else if (format.type === "range") {
const columnMin = name =>
columnExtents && columnExtents[name] && columnExtents[name][0];
const columnMax = name =>
columnExtents && columnExtents[name] && columnExtents[name][1];
const min =
format.min_type === "custom"
? format.min_value
: format.min_type === "all"
? Math.min(...format.columns.map(columnMin))
: columnMin(columnName);
const max =
format.max_type === "custom"
? format.max_value
: format.max_type === "all"
? Math.max(...format.columns.map(columnMax))
: columnMax(columnName);
if (typeof max !== "number" || typeof min !== "number") {
console.warn("Invalid range min/max", min, max);
return () => null;
}
return getColorScale(
[min, max],
format.colors.map(c => alpha(c, GRADIENT_ALPHA)),
).clamp(true);
} else {
console.warn("Unknown format type", format.type);
return () => null;
}
}
function computeColumnExtents(formats, data) {
return _.chain(formats)
.map(format => format.columns)
.flatten()
.uniq()
.map(columnName => {
const colIndex = _.findIndex(data.cols, col => col.name === columnName);
return [columnName, d3.extent(data.rows, row => row[colIndex])];
})
.object()
.value();
}
function compileFormatters(formats, columnExtents) {
const formatters = {};
for (const format of formats) {
for (const columnName of format.columns) {
formatters[columnName] = formatters[columnName] || [];
formatters[columnName].push(
compileFormatter(format, columnName, columnExtents, false),
);
}
}
return formatters;
}
function compileRowFormatters(formats) {
const rowFormatters = [];
for (const format of formats.filter(
format => format.type === "single" && format.highlight_row,
)) {
const formatter = compileFormatter(format, null, null, true);
if (formatter) {
for (const colName of format.columns) {
rowFormatters.push((row, colIndexes) =>
formatter(row[colIndexes[colName]]),
);
}
}
}
return rowFormatters;
}
......@@ -18,9 +18,10 @@ import ChartSettingsTableFormatting, {
isFormattable,
} from "metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx";
import { makeCellBackgroundGetter } from "metabase/visualizations/lib/table_format";
import _ from "underscore";
import cx from "classnames";
import d3 from "d3";
import Color from "color";
import { getColorScale } from "metabase/lib/colors";
......@@ -30,10 +31,6 @@ import { getIn } from "icepick";
import type { DatasetData } from "metabase/meta/types/Dataset";
import type { Card, VisualizationSettings } from "metabase/meta/types/Card";
const CELL_ALPHA = 0.65;
const ROW_ALPHA = 0.2;
const GRADIENT_ALPHA = 0.75;
type Props = {
card: Card,
data: DatasetData,
......@@ -44,115 +41,6 @@ type State = {
data: ?DatasetData,
};
const alpha = (color, amount) =>
Color(color)
.alpha(amount)
.string();
function compileFormatter(
format,
columnName,
columnExtents,
isRowFormatter = false,
) {
if (format.type === "single") {
let { operator, value, color } = format;
if (isRowFormatter) {
color = alpha(color, ROW_ALPHA);
} else {
color = alpha(color, CELL_ALPHA);
}
switch (operator) {
case "<":
return v => (v < value ? color : null);
case "<=":
return v => (v <= value ? color : null);
case ">=":
return v => (v >= value ? color : null);
case ">":
return v => (v > value ? color : null);
case "=":
return v => (v === value ? color : null);
case "!=":
return v => (v !== value ? color : null);
}
} else if (format.type === "range") {
const columnMin = name =>
columnExtents && columnExtents[name] && columnExtents[name][0];
const columnMax = name =>
columnExtents && columnExtents[name] && columnExtents[name][1];
const min =
format.min_type === "custom"
? format.min_value
: format.min_type === "all"
? Math.min(...format.columns.map(columnMin))
: columnMin(columnName);
const max =
format.max_type === "custom"
? format.max_value
: format.max_type === "all"
? Math.max(...format.columns.map(columnMax))
: columnMax(columnName);
if (typeof max !== "number" || typeof min !== "number") {
console.warn("Invalid range min/max", min, max);
return () => null;
}
return getColorScale(
[min, max],
format.colors.map(c => alpha(c, GRADIENT_ALPHA)),
).clamp(true);
} else {
console.warn("Unknown format type", format.type);
return () => null;
}
}
function computeColumnExtents(formats, data) {
return _.chain(formats)
.map(format => format.columns)
.flatten()
.uniq()
.map(columnName => {
const colIndex = _.findIndex(data.cols, col => col.name === columnName);
return [columnName, d3.extent(data.rows, row => row[colIndex])];
})
.object()
.value();
}
function compileFormatters(formats, columnExtents) {
const formatters = {};
for (const format of formats) {
for (const columnName of format.columns) {
formatters[columnName] = formatters[columnName] || [];
formatters[columnName].push(
compileFormatter(format, columnName, columnExtents, false),
);
}
}
return formatters;
}
function compileRowFormatters(formats) {
const rowFormatters = [];
for (const format of formats.filter(
format => format.type === "single" && format.highlight_row,
)) {
const formatter = compileFormatter(format, null, null, true);
if (formatter) {
for (const colName of format.columns) {
rowFormatters.push((row, colIndexes) =>
formatter(row[colIndexes[colName]]),
);
}
}
}
return rowFormatters;
}
export default class Table extends Component {
props: Props;
state: State;
......@@ -222,48 +110,7 @@ export default class Table extends Component {
},
"table._cell_background_getter": {
getValue([{ data }], settings) {
const { rows, cols } = data;
const formats = settings["table.column_formatting"];
const pivot = settings["table.pivot"];
let formatters = {};
let rowFormatters = [];
try {
const columnExtents = computeColumnExtents(formats, data);
formatters = compileFormatters(formats, columnExtents);
rowFormatters = compileRowFormatters(formats, columnExtents);
} catch (e) {
console.error(e);
}
const colIndexes = _.object(
cols.map((col, index) => [col.name, index]),
);
if (
Object.values(formatters).length === 0 &&
Object.values(formatters).length === 0
) {
return null;
} else {
return function(value, rowIndex, colName) {
if (formatters[colName]) {
// const value = rows[rowIndex][colIndexes[colName]];
for (const formatter of formatters[colName]) {
const color = formatter(value);
if (color != null) {
return color;
}
}
}
// don't highlight row for pivoted tables
if (!pivot) {
for (const rowFormatter of rowFormatters) {
const color = rowFormatter(rows[rowIndex], colIndexes);
if (color != null) {
return color;
}
}
}
};
}
return makeCellBackgroundGetter(data, settings);
},
readDependencies: ["table.column_formatting", "table.pivot"],
},
......
......@@ -183,6 +183,7 @@
"build-watch": "yarn && webpack --watch",
"build-hot": "yarn && NODE_ENV=hot webpack-dev-server --progress",
"build-stats": "yarn && webpack --json > stats.json",
"build-shared": "yarn && webpack --config webpack.shared.config.js",
"start": "yarn build && lein ring server",
"precommit": "lint-staged",
"preinstall":
......
This diff is collapsed.
......@@ -2,6 +2,7 @@
"Namespaces that uses the Nashorn javascript engine to invoke some shared javascript code that we use to determine
the background color of pulse table cells"
(:require [clojure.walk :as walk]
[cheshire.core :as json]
[puppetlabs.i18n.core :refer [trs]]
[schema.core :as s])
(:import java.io.InputStream
......@@ -46,7 +47,7 @@
[query-results :- QueryResults, viz-settings]
(let [^Invocable engine @js-engine
;; Keyword strings don't serialize correctly when being passed to the JS engine
js-fn-args (object-array [(walk/stringify-keys query-results) (walk/stringify-keys viz-settings)])]
js-fn-args (object-array [(json/generate-string query-results) (json/generate-string viz-settings)])]
(.invokeFunction engine "makeCellBackgroundGetter" js-fn-args)))
(defn get-background-color
......
const path = require("path");
const config = require("./webpack.config.js");
module.exports = {
entry: {
color_selector: "./frontend/src/metabase-shared/color_selector.js",
},
module: config.module,
resolve: config.resolve,
output: {
path: path.resolve(__dirname, "resources", "frontend_shared"),
filename: "[name].js",
library: "shared",
libraryTarget: "umd",
},
};
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