Skip to content

Commit

Permalink
Refactor index.ts file to extract methods (#65)
Browse files Browse the repository at this point in the history
* Update getSorts method

* Extract derivePlotOptions to separate file

* Add mockDuckPlot and derivePlotOptions files

* Pass instance to processRawData

* Pass instance to preprareChartData

* Move getMarkOptions to own file

* Move click even to legendCategorical

* Pass instance into legendContinuous

* Move handleProperty to its own file

* Organize into folders

* Remove unused import

* Move getMarks to its own file

* Rename markType to mark

* Move visibleSeries default logic into legendCategorical

* Extract render into its own file

* Add some light error handling

* Remove getter/setters that just reset variables

* Fix error catchign

* Rename chartData to data, fix tests

* Reorganize functions

* Reorganizing

* Clarify primaryMark logic, pass through r and fx options

* Remove mock-duck plot, make options sync functions
  • Loading branch information
mkfreeman authored Nov 21, 2024
1 parent 87114a2 commit 623c1bb
Show file tree
Hide file tree
Showing 26 changed files with 948 additions and 768 deletions.
1 change: 0 additions & 1 deletion examples/index.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ for (const [name, plot] of Object.entries(plots)) {
try {
// Wrap the plot function call and the rest of the operations in a try block
const plt = await plot({ jsdom, font });

// Clear the body content before generating a new plot
jsdom.window.document.body.innerHTML = "";

Expand Down
1 change: 1 addition & 0 deletions examples/plots/colorSchemeContinuous.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ duckplot
.color("Open")
.mark("dot")
.options({
width: 600,
color: {
scheme: "blues"
}
Expand Down
12 changes: 12 additions & 0 deletions examples/plots/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { renderPlot } from "../util/renderPlotClient.js";
// This code is both displayed in the browser and executed
const codeString = `// No mark specified so render should error
duckplot
.table("stocks")
.x("Date")
.y("Close")
.fy("Symbol")
.options({ height: 300})
`;

export const error = (options) => renderPlot("stocks.csv", codeString, options);
1 change: 1 addition & 0 deletions examples/plots/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from "./colorScheme.js";
export * from "./colorSchemeContinuous.js";
export * from "./configOptions.js";
export * from "./dot.js";
export * from "./error.js";
export * from "./fy.js";
export * from "./groupedBarMultiY.js";
export * from "./groupedBarMultiYReorder.js";
Expand Down
114 changes: 66 additions & 48 deletions src/prepareChartData.ts → src/data/prepareData.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,79 @@
import { AsyncDuckDB } from "@duckdb/duckdb-wasm";
import {
Aggregate,
ChartData,
ChartType,
Data,
ColumnConfig,
ColumnType,
DescribeSchema,
Indexable,
QueryMap,
} from "./types";
} from "../types";
import {
columnIsDefined,
getAggregateInfo,
getLabel,
getTransformQuery,
toTitleCase,
} from "./query";
import { runQuery } from "./runQuery";
import {
allowAggregation,
formatResults,
checkDistinct,
columnTypes,
} from "./helpers";
import { Database } from "duckdb-async";
} from "../helpers";
import type { DuckPlot } from "..";
import { isColor } from "../options/getPlotOptions";

export function getUniqueName() {
return Date.now().toString(36) + Math.random().toString(36).substring(2);
}

// Query the local duckdb database and format the result based on the settings
export async function prepareChartData(
ddb: AsyncDuckDB | Database,
tableName: string | undefined,
config: ColumnConfig,
type: ChartType,
preQuery?: string,
aggregate?: Aggregate,
percent?: boolean
): Promise<{ data: ChartData; description: string; queries?: QueryMap }> {
export async function prepareData(
instance: DuckPlot
): Promise<{ data: Data; description: string; queries?: QueryMap }> {
let queries: QueryMap = {};
if (!ddb || !tableName)
if (!instance.ddb || !instance.table())
return { data: [], description: "No database or table provided" };
let description = {
value: "",
};

let queryString: string;
let labels: ChartData["labels"] = {};
let labels: Data["labels"] = {};
let preQueryTableName = "";
const reshapeTableName = getUniqueName();

// Identify the columns present in the config:
const columns = Object.fromEntries(
[
["x", instance.x().column],
["y", instance.y().column],
[
"series",
!isColor(instance.color().column) ? instance.color().column : false,
],
["fy", instance.fy().column],
["fx", instance.fx().column],
["r", instance.r().column],
["text", instance.text().column],
].filter(([key, value]) => value) // Remove `false` or `undefined` entries
);

// If someone wants to run some arbitary sql first, store that in a temp table
const preQuery = instance.query();
if (preQuery) {
preQueryTableName = getUniqueName();
const createStatement = `CREATE TABLE ${preQueryTableName} as ${preQuery}`;
description.value += `The provided sql query was run.\n`;
queries["preQuery"] = createStatement;
await runQuery(ddb, createStatement);
await runQuery(instance.ddb, createStatement);
}
let transformTableFrom = preQuery ? preQueryTableName : tableName;
let transformTableFrom = preQuery ? preQueryTableName : instance.table();

// Make sure that the columns are in the schema
const initialSchema = await runQuery(ddb, `DESCRIBE ${transformTableFrom}`);
const allColumns = Object.entries(config).flatMap(([key, col]) => col);
const initialSchema = await runQuery(
instance.ddb,
`DESCRIBE ${transformTableFrom}`
);
const allColumns = Object.entries(columns).flatMap(([key, col]) => col);
const schemaCols = initialSchema.map((row: Indexable) => row.column_name);

// Find the missing columns
Expand All @@ -76,35 +86,43 @@ export async function prepareChartData(
}
// First, reshape the data if necessary: this will create a NEW DUCKDB TABLE
// that has generic column names (e.g., `x`, `y`, `series`, etc.)

const tranformQuery = getTransformQuery(
type,
config,
instance.mark().type,
columns,
transformTableFrom,
reshapeTableName,
description
);
queries["transform"] = tranformQuery;
await runQuery(ddb, tranformQuery);
await runQuery(instance.ddb, tranformQuery);

// Detect if the values are distincy across the other columns, for example if
// the y values are distinct by x, series, and facets for a barY chart. Note,
// the `r` and `label` columns are not considered for distinct-ness but are
// passed through for usage
let distinctCols = (
type === "barX" ? ["y", "series", "fy", "fx"] : ["x", "series", "fy", "fx"]
).filter((d) => columnIsDefined(d as keyof ColumnConfig, config));
instance.mark().type === "barX"
? ["y", "series", "fy", "fx"]
: ["x", "series", "fy", "fx"]
).filter((d) => columnIsDefined(d as keyof ColumnConfig, columns));

// Catch for reshaped data where series gets added
const yValue = Array.isArray(config.y)
? config.y.filter((d) => d)
: [config.y];
const yValue = Array.isArray(columns.y)
? columns.y.filter((d: any) => d)
: [columns.y];
if (yValue.length > 1 && !distinctCols.includes("series")) {
distinctCols.push("series");
}
// Deteremine if we should aggregate

const isDistinct = await checkDistinct(ddb, reshapeTableName, distinctCols);
const allowsAggregation = allowAggregation(type) || aggregate;
const isDistinct = await checkDistinct(
instance.ddb,
reshapeTableName,
distinctCols
);
const allowsAggregation =
allowAggregation(instance.mark().type) || instance.config().aggregate;

// If there are no distinct columns (e.g., y axis is selected without x axis), we can't aggregate
const shouldAggregate =
Expand All @@ -113,45 +131,45 @@ export async function prepareChartData(
(distinctCols.includes("y") ||
distinctCols.includes("x") ||
distinctCols.includes("fx") ||
aggregate);
instance.config().aggregate);
// TODO: do we need the distincCols includes check here...?
const transformedTypes = await columnTypes(ddb, reshapeTableName);
const transformedTypes = await columnTypes(instance.ddb, reshapeTableName);
// TODO: more clear arguments in here
const { labels: aggregateLabels, queryString: aggregateQuery } =
getAggregateInfo(
type,
config,
instance.mark().type,
columns,
[...transformedTypes.keys()],
reshapeTableName,
!shouldAggregate ? false : aggregate,
!shouldAggregate ? false : instance.config().aggregate,
description,
percent
instance.config().percent
);
queryString = aggregateQuery;
labels = aggregateLabels;
let data;
let schema: DescribeSchema;
queries["final"] = queryString;
data = await runQuery(ddb, queryString);
schema = await runQuery(ddb, `DESCRIBE ${reshapeTableName}`);
data = await runQuery(instance.ddb, queryString);
schema = await runQuery(instance.ddb, `DESCRIBE ${reshapeTableName}`);
// Format data as an array of objects
let formatted: ChartData = formatResults(data, schema);
let formatted: Data = formatResults(data, schema);

if (!labels!.series) {
labels!.series = getLabel(config.series);
labels!.series = getLabel(columns.series);
}
if (!labels!.x) {
// Use the fx label for grouped bar charts
labels!.x = getLabel(config.fx ?? config.x);
labels!.x = getLabel(columns.fx ?? columns.x);
}
if (!labels!.y) {
labels!.y = getLabel(config.y);
labels!.y = getLabel(columns.y);
}
formatted.labels = labels;
// Drop the reshaped table
await runQuery(ddb, `drop table if exists "${reshapeTableName}"`);
await runQuery(instance.ddb, `drop table if exists "${reshapeTableName}"`);
if (preQueryTableName)
await runQuery(ddb, `drop table if exists "${preQueryTableName}"`);
await runQuery(instance.ddb, `drop table if exists "${preQueryTableName}"`);
return {
data: formatted,
description:
Expand Down
8 changes: 4 additions & 4 deletions src/query.ts → src/data/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import {
Column,
ColumnConfig,
ChartType,
ChartData,
Data,
Aggregate,
QueryMap,
ColumnType,
} from "./types";
} from "../types";

// Quick helper
const hasProperty = (prop?: ColumnType): boolean =>
Expand Down Expand Up @@ -199,14 +199,14 @@ export function getAggregateInfo(
aggregate: Aggregate | undefined, // TODO: add tests
description: { value: string },
percent?: boolean
): { queryString: string; labels: ChartData["labels"] } {
): { queryString: string; labels: Data["labels"] } {
// Ensure that the x and y values are arrays
const y = arrayIfy(config.y);
const x = arrayIfy(config.x);
const agg = aggregate ?? "sum";
let aggregateSelection;
let groupBy: string[] = [];
let labels: ChartData["labels"] = {};
let labels: Data["labels"] = {};

// Handling horizontal bar charts differently (aggregate on x-axis)
if (type === "barX") {
Expand Down
File renamed without changes.
38 changes: 38 additions & 0 deletions src/handleProperty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { PlotOptions } from "@observablehq/plot";
import type { DuckPlot } from ".";
import { IncomingColumType, PlotProperty } from "./types";
import equal from "fast-deep-equal";
import { isColor } from "./options/getPlotOptions";

export function handleProperty<T extends keyof PlotOptions>(
instance: DuckPlot,
prop: PlotProperty<T>,
column?: IncomingColumType,
options?: PlotOptions[T],
propertyName?: string
): PlotProperty<T> | DuckPlot {
// Because we store empty string for falsey values, we need to check them
const columnValue = column === false || column === null ? "" : column;

if (column !== undefined && !equal(columnValue, prop.column)) {
// Special case handling that we don't need data if color is/was a color
if (
!(
propertyName === "color" &&
isColor(prop.column) &&
typeof column === "string" &&
isColor(column)
)
) {
instance.newDataProps = true; // When changed, we need to requery the data
}
}
if (column === false || column === null) {
prop.column = "";
prop.options = undefined;
} else {
if (column !== undefined) prop.column = column;
if (options !== undefined) prop.options = options;
}
return column === undefined ? prop : instance;
}
Loading

0 comments on commit 623c1bb

Please sign in to comment.