Compare commits

...

13 Commits

Author SHA1 Message Date
Noah Moss
1618472de1 tweaks: default metric, dynamic leafDepth, better breadcrumb 2024-09-18 15:03:32 -05:00
Noah Moss
817b1cb665 disable pan 2024-09-18 13:55:18 -05:00
Noah Moss
0530f0386a limit leaf depth 2024-09-18 13:51:05 -05:00
Noah Moss
829705651d color saturation at the second level 2024-09-18 13:45:56 -05:00
Noah Moss
0b8c08a103 tooltip colors + formatting 2024-09-18 13:20:35 -05:00
Noah Moss
8d7a9a56d2 add colorSaturation 2024-09-18 13:20:35 -05:00
Noah Moss
fad2918ceb basic tooltip 2024-09-18 13:20:35 -05:00
Aleksandr Lesnenko
dbd74da8ac add another 2024-09-18 13:20:35 -05:00
Noah Moss
13200bffe2 treemap hierarchy 2024-09-18 13:20:35 -05:00
Aleksandr Lesnenko
d8f6f3c32f icon 2024-09-18 13:20:35 -05:00
Aleksandr Lesnenko
5f25159e9b settings 2024-09-18 13:20:35 -05:00
Noah Moss
acab629ef4 one-dimension 2024-09-18 13:20:35 -05:00
Aleksandr Lesnenko
b8423734ff treemap boilerplate 2024-09-18 13:20:35 -05:00
8 changed files with 201 additions and 0 deletions

View File

@@ -32,6 +32,7 @@ const cardDisplayTypes = [
"map",
"scatter",
"waterfall",
"treemap",
] as const;
export const isCardDisplayType = (value: unknown): value is CardDisplayType =>

View File

@@ -18,4 +18,5 @@ export const DEFAULT_VIZ_ORDER: CardDisplayType[] = [
"map",
"scatter",
"waterfall",
"treemap",
] as const;

View File

@@ -363,6 +363,8 @@ import trash_component from "./trash.svg?component";
import trash_source from "./trash.svg?source";
import trash_filled_component from "./trash_filled.svg?component";
import trash_filled_source from "./trash_filled.svg?source";
import treemap_component from "./treemap.svg?component";
import treemap_source from "./treemap.svg?source";
import triangle_left_component from "./triangle_left.svg?component";
import triangle_left_source from "./triangle_left.svg?source";
import triangle_right_component from "./triangle_right.svg?component";
@@ -1001,6 +1003,10 @@ export const Icons = {
component: sync_component,
source: sync_source,
},
treemap: {
component: treemap_component,
source: treemap_source,
},
question: {
component: question_component,
source: question_source,

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="currentcolor" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2h12v12H2V2zm1 1v4h4V3H3zm5 0v7h5V3H8zm0 8v2h5v-2H8zm-5 2h4v-6H3v6z"/>
</svg>

After

Width:  |  Height:  |  Size: 173 B

View File

@@ -1,12 +1,14 @@
import {
BarChart,
CustomChart,
TreemapChart,
LineChart,
ScatterChart,
SunburstChart,
} from "echarts/charts";
import {
BrushComponent,
CalendarComponent,
DataZoomComponent,
DatasetComponent,
GraphicComponent,
@@ -14,6 +16,7 @@ import {
MarkLineComponent,
ToolboxComponent,
TooltipComponent,
VisualMapComponent,
} from "echarts/components";
import { use } from "echarts/core";
import { LabelLayout } from "echarts/features";
@@ -26,6 +29,7 @@ export const registerEChartsModules = () => {
ScatterChart,
CustomChart,
SunburstChart,
TreemapChart,
GraphicComponent,
GridComponent,
BarChart,
@@ -37,5 +41,7 @@ export const registerEChartsModules = () => {
DatasetComponent,
LabelLayout,
TooltipComponent,
CalendarComponent,
VisualMapComponent,
]);
};

View File

@@ -11,6 +11,7 @@ import { DashCardPlaceholder } from "./visualizations/DashCardPlaceholder";
import { Funnel } from "./visualizations/Funnel";
import Gauge from "./visualizations/Gauge";
import { Heading } from "./visualizations/Heading";
import { Treemap } from "./visualizations/Treemap";
import { LineChart } from "./visualizations/LineChart";
import { LinkViz } from "./visualizations/LinkViz";
import { Map } from "./visualizations/Map";
@@ -44,6 +45,7 @@ export default function () {
registerVisualization(Funnel);
registerVisualization(ObjectDetail);
registerVisualization(PivotTable);
registerVisualization(Treemap);
registerVisualization(ActionViz);
registerVisualization(DashCardPlaceholder);

View File

@@ -0,0 +1,181 @@
import type { TreemapSeriesOption } from "echarts";
import type { TooltipOption } from "echarts/types/dist/shared";
import { t } from "ttag";
import _ from "underscore";
import { findWithIndex } from "metabase/lib/arrays";
import { ResponsiveEChartsRenderer } from "metabase/visualizations/components/EChartsRenderer";
import { columnSettings } from "metabase/visualizations/lib/settings/column";
import {
getOptionFromColumn,
metricSetting,
} from "metabase/visualizations/lib/settings/utils";
import { getDefaultDimensionsAndMetrics } from "metabase/visualizations/lib/utils";
import type { VisualizationProps } from "metabase/visualizations/types";
import type { RawSeries, RowValue, RowValues } from "metabase-types/api";
const MAX_TREEMAP_DIMENSIONS = 100;
// Defines supported visualization settings
const SETTING_DEFINITIONS = {
// Column formatting settings
...columnSettings({ hidden: true }),
// Treemap dimensions
"treemap.dimensions": {
section: t`Data`,
title: t`Dimensions`,
widget: "fields",
getDefault: (rawSeries: RawSeries) =>
getDefaultDimensionsAndMetrics(rawSeries, MAX_TREEMAP_DIMENSIONS, 1)
.dimensions,
persistDefault: true,
getProps: ([{ data }]: any) => {
const options = data.cols.map(getOptionFromColumn);
return {
options,
addAnother: t`Add dimension`,
columns: data.cols,
};
},
},
// Heatmap metric column
...metricSetting("treemap.metric", {
section: t`Data`,
title: t`Measure`,
showColumnSetting: true,
getDefault: (rawSeries: RawSeries) =>
getDefaultDimensionsAndMetrics(rawSeries, MAX_TREEMAP_DIMENSIONS, 1)
.metrics[0],
}),
};
interface TreeNode {
name: string;
value: number;
children: TreeNode[];
childrenMap: Map<any, TreeNode>;
}
const buildTree = (
rows: RowValues[],
dimensions: any,
metricIndex: number,
): TreeNode[] => {
const root: TreeNode = {
name: "Root",
value: 0,
children: [],
childrenMap: new Map(),
};
rows.forEach((row: RowValues) => {
let currentNode = root;
const metricValue = row[metricIndex];
if (typeof metricValue !== "number") {
throw new Error(t`Treemap visualization requires a numerical metric.`);
}
dimensions.forEach((dimension: any) => {
const value: RowValue = row[dimension.index];
let node: TreeNode;
if (currentNode.childrenMap.has(value)) {
node = currentNode.childrenMap.get(value)!;
} else {
node = {
name: String(value),
value: 0,
children: [],
childrenMap: new Map(),
};
currentNode.children.push(node);
currentNode.childrenMap.set(value, node);
}
node.value += metricValue;
currentNode = node;
});
});
// Remove the childrenMap property from all nodes to clean up the final output
const clean = (node: any) => {
delete node.childrenMap;
if (node.children.length === 0) {
delete node.children;
} else {
node.children.forEach((child: any) => clean(child));
}
};
clean(root);
return root.children;
};
export const Treemap = ({ rawSeries, settings }: VisualizationProps) => {
const [{ data }] = rawSeries;
const dimensions = settings["treemap.dimensions"].map((dimName: string) => {
return findWithIndex(data.cols, col => col.name === dimName);
});
const metric = findWithIndex(
data.cols,
col => col.name === settings["treemap.metric"],
);
const echartsData = buildTree(data.rows, dimensions, metric.index);
const treemapSeries: TreemapSeriesOption = {
type: "treemap",
name: "All",
data: echartsData,
roam: true,
leafDepth: dimensions.length > 2 ? 1 : undefined,
itemStyle: {
gapWidth: 2,
},
levels: [
{},
{
colorSaturation: [0.25, 0.65],
},
{},
],
};
const treemapTooltip: TooltipOption = {
textStyle: {
color: "white",
},
backgroundColor: "#2e353b",
borderColor: "transparent",
padding: 10,
formatter: (info: any) => {
// Build the breadcrumb by traversing ancestors
const breadcrumb = info.treePathInfo
.slice(1)
.map((n: any) => n.name)
.join(" > ");
const value = info.data.value;
return `${breadcrumb}<br/>Value: ${value}`;
},
};
const option = {
series: [treemapSeries],
tooltip: treemapTooltip,
};
return <ResponsiveEChartsRenderer option={option} />;
};
Object.assign(Treemap, {
uiName: t`Treemap`,
identifier: "treemap",
iconName: "treemap",
noun: t`Treemap`,
settings: SETTING_DEFINITIONS,
});

View File

@@ -0,0 +1 @@
export * from "./Treemap";