Skip to content
Snippets Groups Projects
Unverified Commit 055a4d5f authored by Tom Robinson's avatar Tom Robinson
Browse files

Row chart

parent f555c7b5
No related branches found
No related tags found
No related merge requests found
Showing
with 206 additions and 35 deletions
......@@ -253,6 +253,13 @@ export var ICON_PATHS = {
ICON_PATHS["illustration-line"] = ICON_PATHS['illustration-area'];
ICON_PATHS["illustration-scatter"] = ICON_PATHS['illustration-area'];
ICON_PATHS["horizontal_bar"] = {
path: ICON_PATHS["bar"],
attrs: {
style: { transform: "rotate(90deg) scaleX(-1)" }
}
};
export function loadIcon(name) {
var def = ICON_PATHS[name];
if (!def) {
......
......@@ -666,6 +666,7 @@ const SETTINGS_PREFIXES_BY_CHART_TYPE = {
line: ["graph.", "line."],
area: ["graph.", "line.", "stackable."],
bar: ["graph.", "stackable."],
row: ["graph.dimensions", "graph.metrics", "graph.colors", "graph.y_axis.labels_enabled", "graph.y_axis.title_text"],
scatter: ["graph.", "scatter."],
pie: ["pie."],
scalar: ["scalar."],
......
......@@ -3,10 +3,13 @@
import React, { Component, PropTypes } from "react";
import LineAreaBarChart from "./components/LineAreaBarChart.jsx";
import { areaRenderer } from "./lib/LineAreaBarRenderer";
export default class AreaChart extends LineAreaBarChart {
static uiName = "Area";
static identifier = "area";
static iconName = "area";
static noun = "area chart";
static renderer = areaRenderer;
}
......@@ -3,10 +3,13 @@
import React, { Component, PropTypes } from "react";
import LineAreaBarChart from "./components/LineAreaBarChart.jsx";
import { barRenderer } from "./lib/LineAreaBarRenderer";
export default class BarChart extends LineAreaBarChart {
static uiName = "Bar";
static identifier = "bar";
static iconName = "bar";
static noun = "bar chart";
static renderer = barRenderer;
}
......@@ -3,10 +3,13 @@
import React, { Component, PropTypes } from "react";
import LineAreaBarChart from "./components/LineAreaBarChart.jsx";
import { lineRenderer } from "./lib/LineAreaBarRenderer";
export default class LineChart extends LineAreaBarChart {
static uiName = "Line";
static identifier = "line";
static iconName = "line";
static noun = "line chart";
static renderer = lineRenderer;
}
/* @flow */
import React, { Component, PropTypes } from "react";
import dc from "dc";
import LineAreaBarChart from "./components/LineAreaBarChart.jsx";
import { rowRenderer } from "./lib/LineAreaBarRenderer";
export default class BarChart extends LineAreaBarChart {
static uiName = "Row Chart";
static identifier = "row";
static iconName = "horizontal_bar";
static noun = "row chart";
static supportsSeries = false;
static renderer = rowRenderer;
}
......@@ -3,10 +3,13 @@
import React, { Component, PropTypes } from "react";
import LineAreaBarChart from "./components/LineAreaBarChart.jsx";
import { scatterRenderer } from "./lib/LineAreaBarRenderer";
export default class ScatterPlot extends LineAreaBarChart {
static uiName = "Scatter";
static identifier = "scatter";
static iconName = "bubble";
static noun = "scatter plot";
static renderer = scatterRenderer;
}
......@@ -3,6 +3,7 @@
margin-left: -0.5em;
margin-right: -0.5em;
margin-bottom: -0.5em;
overflow: hidden;
}
/* TODO (@kdoh) remove this if dc-js/dc.js#1260 is merged */
......@@ -27,6 +28,13 @@
fill: #C5C6C8;
}
.LineAreaBarChart .dc-chart g.row text.outside {
fill: #C5C6C8;
}
.LineAreaBarChart .dc-chart g.row text.inside {
fill: white;
}
/* turn off ticks and domain lines */
.LineAreaBarChart .dc-chart .axis.y .domain,
.LineAreaBarChart .dc-chart .axis.yr .domain,
......@@ -54,8 +62,14 @@
.LineAreaBarChart .dc-chart rect.bar:hover {
fill-opacity: 1;
}
/* highlight single series bar */
.LineAreaBarChart.mute-0 .dc-chart rect.bar:hover {
.LineAreaBarChart .dc-chart g.row rect {
fill-opacity: 1;
}
/* highlight single series bar and row charts */
.LineAreaBarChart.mute-0 .dc-chart rect.bar:hover,
.LineAreaBarChart.mute-0 .dc-chart g.row:hover {
opacity: 1 !important;
}
......@@ -92,8 +106,10 @@
.LineAreaBarChart .dc-chart .area,
.LineAreaBarChart .dc-chart .bar,
.LineAreaBarChart .dc-chart .line,
.LineAreaBarChart .dc-chart .dot {
transition: opacity 0.1s linear;
.LineAreaBarChart .dc-chart .dot,
.LineAreaBarChart .dc-chart .row,
.LineAreaBarChart .dc-chart .bubble {
transition: opacity 0.15s linear;
}
.LineAreaBarChart .dc-chart .axis.y,
......
......@@ -32,10 +32,14 @@ for (let i = 0; i < MAX_SERIES; i++) {
addCSSRule(`.LineAreaBarChart.mute-${i} svg.stacked .stack._${i} .line`, MUTE_STYLE);
addCSSRule(`.LineAreaBarChart.mute-${i} svg.stacked .stack._${i} .bar`, MUTE_STYLE);
addCSSRule(`.LineAreaBarChart.mute-${i} svg.stacked .dc-tooltip._${i} .dot`, MUTE_STYLE);
addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .bar`, MUTE_STYLE);
addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .line`, MUTE_STYLE);
addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .dot`, MUTE_STYLE);
addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .bubble`, MUTE_STYLE);
// row charts don't support multiseries
addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .row`, MUTE_STYLE);
}
import type { VisualizationProps } from "metabase/visualizations";
......@@ -124,10 +128,6 @@ export default class LineAreaBarChart extends Component<*, VisualizationProps, *
}
}
getChartType() {
return this.constructor.identifier;
}
getFidelity() {
let fidelity = { x: 0, y: 0 };
let size = this.props.gridSize || { width: Infinity, height: Infinity };
......@@ -218,12 +218,11 @@ export default class LineAreaBarChart extends Component<*, VisualizationProps, *
: null }
<CardRenderer
{...this.props}
chartType={this.getChartType()}
series={series}
settings={settings}
className="renderer flex-full"
maxSeries={MAX_SERIES}
renderer={lineAreaBarRenderer}
renderer={this.constructor.renderer}
/>
<ChartTooltip series={series} hovered={hovered} />
</div>
......
......@@ -7,6 +7,7 @@ import Progress from "./Progress.jsx";
import Table from "./Table.jsx";
import LineChart from "./LineChart.jsx";
import BarChart from "./BarChart.jsx";
import RowChart from "./RowChart.jsx";
import PieChart from "./PieChart.jsx";
import AreaChart from "./AreaChart.jsx";
import MapViz from "./Map.jsx";
......@@ -110,8 +111,9 @@ registerVisualization(Scalar);
registerVisualization(Progress);
registerVisualization(Table);
registerVisualization(LineChart);
registerVisualization(BarChart);
registerVisualization(AreaChart);
registerVisualization(BarChart);
registerVisualization(RowChart);
registerVisualization(ScatterPlot);
registerVisualization(PieChart);
registerVisualization(MapViz);
......
......@@ -81,10 +81,16 @@ function getDcjsChartType(cardType) {
}
}
function applyChartBoundary(chart, element) {
return chart
.width(getAvailableCanvasWidth(element))
.height(getAvailableCanvasHeight(element));
function initChart(chart, element) {
// set the bounds
chart.width(getAvailableCanvasWidth(element));
chart.height(getAvailableCanvasHeight(element));
// disable animations
chart.transitionDuration(0);
// if the chart supports 'brushing' (brush-based range filter), disable this since it intercepts mouse hovers which means we can't see tooltips
if (chart.brushOn) {
chart.brushOn(false);
}
}
function applyChartTimeseriesXAxis(chart, settings, series, xValues, xDomain, xInterval) {
......@@ -870,8 +876,7 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
}
let parent = dc.compositeChart(element);
applyChartBoundary(parent, element);
parent.transitionDuration(0);
initChart(parent, element);
let charts = groups.map((group, index) => {
let chart = dc[getDcjsChartType(chartType)](parent);
......@@ -963,10 +968,10 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
}
}
let chart = parent.compose(charts);
parent.compose(charts);
if (groups.length > 1 && !isScalarSeries) {
chart.on("renderlet.grouped-bar", function (chart) {
parent.on("renderlet.grouped-bar", function (chart) {
// HACK: dc.js doesn't support grouped bar charts so we need to manually resize/reposition them
// https://github.com/dc-js/dc.js/issues/558
let barCharts = chart.selectAll(".sub rect:first-child")[0].map(node => node.parentNode.parentNode.parentNode);
......@@ -989,18 +994,18 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
// HACK: compositeChart + ordinal X axis shenanigans
if (chartType === "bar") {
chart._rangeBandPadding(BAR_PADDING_RATIO) // https://github.com/dc-js/dc.js/issues/678
parent._rangeBandPadding(BAR_PADDING_RATIO) // https://github.com/dc-js/dc.js/issues/678
} else {
chart._rangeBandPadding(1) // https://github.com/dc-js/dc.js/issues/662
parent._rangeBandPadding(1) // https://github.com/dc-js/dc.js/issues/662
}
// x-axis settings
if (isTimeseries) {
applyChartTimeseriesXAxis(chart, settings, series, xValues, xDomain, xInterval);
applyChartTimeseriesXAxis(parent, settings, series, xValues, xDomain, xInterval);
} else if (isQuantitative) {
applyChartQuantitativeXAxis(chart, settings, series, xValues, xDomain, xInterval);
applyChartQuantitativeXAxis(parent, settings, series, xValues, xDomain, xInterval);
} else {
applyChartOrdinalXAxis(chart, settings, series, xValues);
applyChartOrdinalXAxis(parent, settings, series, xValues);
}
// y-axis settings
......@@ -1009,14 +1014,14 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
extent: d3.extent([].concat(...indexes.map(index => yExtents[index])))
}));
if (left && left.series.length > 0) {
applyChartYAxis(chart, settings, left.series, left.extent, "left");
applyChartYAxis(parent, settings, left.series, left.extent, "left");
}
if (right && right.series.length > 0) {
applyChartYAxis(chart, settings, right.series, right.extent, "right");
applyChartYAxis(parent, settings, right.series, right.extent, "right");
}
const isSplitAxis = (right && right.series.length) && (left && left.series.length > 0);
applyChartTooltips(chart, series, isStacked, (hovered) => {
applyChartTooltips(parent, series, isStacked, (hovered) => {
if (onHoverChange) {
// disable tooltips on lines
if (hovered && hovered.element && hovered.element.classList.contains("line")) {
......@@ -1026,16 +1031,11 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
}
});
// if the chart supports 'brushing' (brush-based range filter), disable this since it intercepts mouse hovers which means we can't see tooltips
if (chart.brushOn) {
chart.brushOn(false);
}
// render
chart.render();
parent.render();
// apply any on-rendering functions
lineAndBarOnRender(chart, settings, onGoalHover, isSplitAxis, isStacked);
lineAndBarOnRender(parent, settings, onGoalHover, isSplitAxis, isStacked);
// only ordinal axis can display "null" values
if (isOrdinal) {
......@@ -1047,5 +1047,120 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
warnings: Object.keys(warnings)
});
return chart;
return parent;
}
export const lineRenderer = (element, props) => lineAreaBar(element, { ...props, chartType: "line" });
export const areaRenderer = (element, props) => lineAreaBar(element, { ...props, chartType: "area" });
export const barRenderer = (element, props) => lineAreaBar(element, { ...props, chartType: "bar" });
export const scatterRenderer = (element, props) => lineAreaBar(element, { ...props, chartType: "scatter" });
export function rowRenderer(
element,
{ settings, series, onHoverChange, height }
) {
const chart = dc.rowChart(element);
const colors = settings["graph.colors"];
const dataset = crossfilter(series[0].data.rows);
const dimension = dataset.dimension(d => d[0]);
const group = dimension.group().reduceSum(d => d[1]);
const xDomain = d3.extent(series[0].data.rows, d => d[1]);
initChart(chart, element);
chart.on("renderlet.tooltips", chart => {
chart.selectAll(".row rect").on("mousemove", (d, i) => {
const { cols } = series[0].data;
onHoverChange && onHoverChange({
// for single series bar charts, fade the series and highlght the hovered element with CSS
index: -1,
event: d3.event,
data: [
{ key: getFriendlyName(cols[0]), value: d.key, col: cols[0] },
{ key: getFriendlyName(cols[1]), value: d.value, col: cols[1] }
]
});
}).on("mouseleave", () => {
onHoverChange && onHoverChange(null);
});
});
chart
.ordinalColors([ colors[0] ])
.x(d3.scale.linear().domain(xDomain))
.elasticX(true)
.dimension(dimension)
.group(group)
.ordering(d => d.index);
let labelPadHorizontal = 5;
let labelPadVertical = 2;
let labelsOutside = false;
chart.on("renderlet.bar-labels", chart => {
chart
.selectAll("g.row text")
.attr("text-anchor", labelsOutside ? "end" : "start")
.attr("x", labelsOutside ? -labelPadHorizontal : labelPadHorizontal)
.classed(labelsOutside ? "outside" : "inside", true);
});
if (settings["graph.y_axis.labels_enabled"]) {
chart.on("renderlet.axis-labels", chart => {
chart
.svg()
.append("text")
.attr("class", "x-axis-label")
.attr("text-anchor", "middle")
.attr("x", chart.width() / 2)
.attr("y", chart.height() - 10)
.text(settings["graph.y_axis.title_text"]);
});
}
// inital render
chart.render();
// bottom label height
let axisLabelHeight = 0;
if (settings["graph.y_axis.labels_enabled"]) {
axisLabelHeight = chart
.select(".x-axis-label")
.node()
.getBoundingClientRect().height;
chart.margins().bottom += axisLabelHeight;
}
// cap number of rows to fit
let rects = chart.selectAll(".row rect")[0];
let containerHeight = rects[rects.length - 1].getBoundingClientRect().bottom -
rects[0].getBoundingClientRect().top;
let maxTextHeight = Math.max(
...chart.selectAll("g.row text")[0].map(
e => e.getBoundingClientRect().height
)
);
let rowHeight = maxTextHeight + chart.gap() + labelPadVertical * 2;
let cap = Math.max(1, Math.floor(containerHeight / rowHeight));
chart.cap(cap);
chart.render();
// check if labels overflow after rendering correct number of rows
let maxTextWidth = 0;
for (const elem of chart.selectAll("g.row")[0]) {
let rect = elem.querySelector("rect").getBoundingClientRect();
let text = elem.querySelector("text").getBoundingClientRect();
maxTextWidth = Math.max(maxTextWidth, text.width);
if (rect.width < text.width + labelPadHorizontal * 2) {
labelsOutside = true;
}
}
if (labelsOutside) {
chart.margins().left += maxTextWidth;
chart.render();
}
}
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