Skip to content

Commit

Permalink
feat: timeslip prototype added (#1767)
Browse files Browse the repository at this point in the history
The direct manipulation oriented timeslip prototype converted to a pre-alpha chart type as basis for further work
  • Loading branch information
monfera authored Aug 2, 2022
1 parent 65e3527 commit b079766
Show file tree
Hide file tree
Showing 30 changed files with 2,198 additions and 2 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@
"mini-css-extract-plugin": "1.6.2",
"moment": "^2.29.1",
"moment-timezone": "^0.5.32",
"sass": "^1.49.9",
"numeral": "^2.0.6",
"postcss": "^8.3.0",
"postcss-cli": "^8.3.1",
Expand All @@ -167,6 +166,7 @@
"react-dom": "^16.13.0",
"react-is": "^16.13.0",
"redux-devtools-extension": "^2.13.8",
"sass": "^1.49.9",
"sass-graph": "^3.0.5",
"seedrandom": "^3.0.5",
"semantic-release": "^19.0.3",
Expand Down
32 changes: 32 additions & 0 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ export const ChartType: Readonly<{
Goal: "goal";
Partition: "partition";
Flame: "flame";
Timeslip: "timeslip";
XYAxis: "xy_axis";
Heatmap: "heatmap";
Wordcloud: "wordcloud";
Expand Down Expand Up @@ -1082,6 +1083,18 @@ export interface GeometryValue {
y: any;
}

// @public
export type GetData = (dataDemand: {
lo: TimeBin;
hi: TimeBin;
binUnit: string;
binUnitCount: number;
unitBarMaxWidthPixels: number;
}) => Array<{
epochMs: number;
value: number;
}>;

// @public (undocumented)
export function getNodeName(node: ArrayNode): string;

Expand Down Expand Up @@ -2718,6 +2731,21 @@ export interface TimeScale {
type: typeof ScaleType.Time;
}

// Warning: (ae-forgotten-export) The symbol "buildProps" needs to be exported by the entry point index.d.ts
//
// @public
export const Timeslip: (props: SFProps<TimeslipSpec, keyof (typeof buildProps_2)['overrides'], keyof (typeof buildProps_2)['defaults'], keyof (typeof buildProps_2)['optionals'], keyof (typeof buildProps_2)['requires']>) => null;

// @public
export interface TimeslipSpec extends Spec {
// (undocumented)
chartType: typeof ChartType.Timeslip;
// (undocumented)
getData: GetData;
// (undocumented)
specType: typeof SpecType.Series;
}

// @public
export function toEntries<T extends Record<string, string>, S>(array: T[], accessor: keyof T, staticValue: S): Record<string, S>;

Expand Down Expand Up @@ -3086,6 +3114,10 @@ export interface YDomainBase {
// @public (undocumented)
export type YDomainRange = YDomainBase & DomainRange & LogScaleOptions;

// Warnings were encountered during analysis:
//
// src/chart_types/timeslip/timeslip_api.ts:21:3 - (ae-forgotten-export) The symbol "TimeBin" needs to be exported by the entry point index.d.ts

// (No @packageDocumentation comment for this package)

```
1 change: 1 addition & 0 deletions packages/charts/src/chart_types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const ChartType = Object.freeze({
Goal: 'goal' as const,
Partition: 'partition' as const,
Flame: 'flame' as const,
Timeslip: 'timeslip' as const,
XYAxis: 'xy_axis' as const,
Heatmap: 'heatmap' as const,
Wordcloud: 'wordcloud' as const,
Expand Down
40 changes: 40 additions & 0 deletions packages/charts/src/chart_types/timeslip/internal_chart_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { ChartType } from '..';
import { DEFAULT_CSS_CURSOR } from '../../common/constants';
import { LegendItemExtraValues } from '../../common/legend';
import { SeriesKey } from '../../common/series_id';
import { InternalChartState } from '../../state/chart_state';
import { InitStatus } from '../../state/selectors/get_internal_is_intialized';
import { TimeslipWithTooltip } from './timeslip_chart';

/** @internal */
export class TimeslipState implements InternalChartState {
chartType = ChartType.Timeslip;
getChartTypeDescription = () => 'Timeslip chart';
chartRenderer = TimeslipWithTooltip;

// default empty properties, unused in Timeslip
eventCallbacks = () => {};
isInitialized = () => InitStatus.Initialized;
isBrushAvailable = () => false;
isBrushing = () => false;
isChartEmpty = () => false;
getLegendItemsLabels = () => [];
getLegendItems = () => [];
getLegendExtraValues = () => new Map<SeriesKey, LegendItemExtraValues>();
getPointerCursor = () => DEFAULT_CSS_CURSOR;
getTooltipAnchor = () => ({ x: 0, y: 0, width: 0, height: 0 });
isTooltipVisible = () => ({ visible: false, isExternal: false });
getTooltipInfo = () => ({ header: null, values: [] });
getProjectionContainerArea = () => ({ width: 0, height: 0, top: 0, left: 0 });
getMainProjectionArea = () => ({ width: 0, height: 0, top: 0, left: 0 });
getBrushArea = () => null;
getDebugState = () => ({});
}
51 changes: 51 additions & 0 deletions packages/charts/src/chart_types/timeslip/timeslip/axis_model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

// @ts-noCheck

const getNiceTicksForApproxCount = (domainMin, domainMax, approxDesiredTickCount) => {
const diff = domainMax - domainMin;
const rawPitch = diff / approxDesiredTickCount;
const exponent = Math.floor(Math.log10(rawPitch));
const orderOfMagnitude = 10 ** exponent; // this represents the order of magnitude eg. 10000, so that...
const mantissa = rawPitch / orderOfMagnitude; // it's always the case that 1 <= mantissa <= 9.99999999999
const niceMantissa = mantissa > 5 ? 10 : mantissa > 2 ? 5 : mantissa > 1 ? 2 : 1; // snap to 10, 5, 2 or 1
const tickInterval = niceMantissa * orderOfMagnitude;
if (!isFinite(tickInterval)) {
return [];
}
const result = [];
for (let i = Math.floor(domainMin / tickInterval); i <= Math.ceil(domainMax / tickInterval); i++) {
result.push(i * tickInterval);
}
return result;
};

const getNiceTicks = (domainMin, domainMax, maximumTickCount) => {
let bestCandidate = [];
for (let i = 0; i <= maximumTickCount; i++) {
const candidate = getNiceTicksForApproxCount(domainMin, domainMax, maximumTickCount - i);
if (candidate.length <= maximumTickCount && candidate.length > 0) return candidate;
if (bestCandidate.length === 0 || maximumTickCount - candidate.length < maximumTickCount - bestCandidate.length) {
bestCandidate = candidate;
}
}
return bestCandidate.length > maximumTickCount
? [...(maximumTickCount > 1 ? [bestCandidate[0]] : []), bestCandidate[bestCandidate.length - 1]]
: [];
};

/** @internal */
export const axisModel = (domainLandmarks, desiredTickCount) => {
const domainMin = Math.min(...domainLandmarks);
const domainMax = Math.max(...domainLandmarks);
const niceTicks = getNiceTicks(domainMin, domainMax, desiredTickCount);
const niceDomainMin = niceTicks.length >= 2 ? niceTicks[0] : domainMin;
const niceDomainMax = niceTicks.length >= 2 ? niceTicks[niceTicks.length - 1] : domainMax;
return { niceDomainMin, niceDomainMax, niceTicks };
};
34 changes: 34 additions & 0 deletions packages/charts/src/chart_types/timeslip/timeslip/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

// @ts-noCheck

/** @internal */
export const dataSource = Symbol('dataSource');

/** @internal */
export const getEnrichedData = (rows) => {
const stats = rows.reduce(
(p, { epochMs, value }) => {
const { minEpochMs, maxEpochMs, minValue, maxValue } = p;
p.minEpochMs = Math.min(minEpochMs, epochMs);
p.maxEpochMs = Math.max(maxEpochMs, epochMs);
p.minValue = Math.min(minValue, value);
p.maxValue = Math.max(maxValue, value);
return p;
},
{
minEpochMs: Infinity,
maxEpochMs: -Infinity,
minValue: Infinity,
maxValue: -Infinity,
},
);
// console.log({ from: new Date(stats.minEpochMs), to: new Date(stats.maxEpochMs), count: rows.length })
return { rows, stats };
};
33 changes: 33 additions & 0 deletions packages/charts/src/chart_types/timeslip/timeslip/domain_tween.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

// @ts-noCheck

import { mix } from './utils/math';

const REFERENCE_AF_LENGTH = 16.67; // ms
const REFERENCE_Y_RECURRENCE_ALPHA = 0.1;
const TWEEN_DONE_EPSILON = 0.001;

/** @internal */
export const domainTween = (interactionState, deltaT, targetMin, targetMax) => {
const { niceDomainMin: currentMin, niceDomainMax: currentMax } = interactionState;

// pure logic
const speedExp = Math.pow((currentMax - currentMin) / (targetMax - targetMin), 0.2); // speeds up big decreases
const advance = 1 - (1 - REFERENCE_Y_RECURRENCE_ALPHA) ** ((speedExp * deltaT) / REFERENCE_AF_LENGTH);
const min = Number.isFinite(currentMin) ? mix(currentMin, targetMin, advance) : targetMin;
const max = Number.isFinite(currentMax) ? mix(currentMax, targetMax, advance) : targetMax;
const tweenIncomplete = Math.abs(1 - (max - min) / (targetMax - targetMin)) > TWEEN_DONE_EPSILON;

// remember
interactionState.niceDomainMin = min;
interactionState.niceDomainMax = max;

return tweenIncomplete;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

// @ts-noCheck

/** @internal */
export function renderChartTitle(ctx, config, chartWidth, cartesianTop, aggregationFunctionName) {
ctx.save();
const titleFontSize = 32; // todo move to config
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.font = `normal normal 200 ${titleFontSize}px Inter, Helvetica, Arial, sans-serif`; // todo move to config
ctx.fillStyle = config.subduedFontColor;
ctx.fillText(config.queryConfig.metricFieldName, chartWidth / 2, cartesianTop / 2 - titleFontSize * 0.5);
ctx.fillText(aggregationFunctionName, chartWidth / 2, cartesianTop / 2 + titleFontSize * 0.5);
ctx.restore();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

// @ts-noCheck

/** @internal */
export function renderTimeExtentAnnotation(
ctx,
config,
localeOptions,
timeDomainFrom,
timeDomainTo,
cartesianWidth,
chartTopFontSize,
) {
ctx.save();
ctx.textBaseline = 'bottom';
ctx.textAlign = 'right';
ctx.font = config.monospacedFontShorthand;
ctx.fillStyle = config.subduedFontColor;
// todo switch to new Intl.DateTimeFormat for more performance https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
ctx.fillText(
`${new Date(timeDomainFrom * 1000).toLocaleString(config.locale, localeOptions)}${new Date(
timeDomainTo * 1000,
).toLocaleString(config.locale, localeOptions)}`,
cartesianWidth,
-chartTopFontSize * 0.5,
);
ctx.restore();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

// @ts-noCheck

import { uiStrings } from '../../translations';

/** @internal */
export function renderTimeUnitAnnotation(ctx, config, binUnitCount, binUnit, chartTopFontSize, unitBarMaxWidthPixels) {
ctx.save();
ctx.textBaseline = 'bottom';
ctx.textAlign = 'left';
ctx.font = config.monospacedFontShorthand;
ctx.fillStyle = config.a11y.contrast === 'low' ? config.subduedFontColor : config.defaultFontColor;
ctx.fillText(
`1 ${uiStrings[config.locale].bar} = ${binUnitCount} ${
uiStrings[config.locale][binUnit + (binUnitCount !== 1 ? 's' : '')]
}`,
0,
-chartTopFontSize * 0.5,
);
const unitBarY = -chartTopFontSize * 2.2;
ctx.fillRect(0, unitBarY, unitBarMaxWidthPixels, 1);
ctx.fillRect(0, unitBarY - 3, 1, 7);
ctx.fillRect(unitBarMaxWidthPixels - 1, unitBarY - 3, 1, 7);
ctx.restore();
}
Loading

0 comments on commit b079766

Please sign in to comment.