* add vega-morphcharts

* add vega-morphcharts-test-es6

* fix sanddance-test-es6

* remove comment

* remove jsdoc

* remove unused constants

* use key instead of keyCode

* remove comment

* remove comment

* reset version

* remove area mark / polygons
This commit is contained in:
Dan Marshall 2022-07-11 11:57:31 -07:00 коммит произвёл GitHub
Родитель 39bc8365ca
Коммит 5143bd67f6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
57 изменённых файлов: 87264 добавлений и 4941 удалений

Просмотреть файл

@ -10,12 +10,8 @@ title: Examples
### sanddance-specs
* [sanddance-specs 2D vega specs tests](../tests/sanddance-specs/v1/) using UMD/CDN
### cube-layer
* [cubeTest](../tests/v3/umd/cubeTest.html) using UMD/CDN
### vega-deck.gl
* [simple vega spec](../tests/v3/umd/vega-deck.gl.test.html) using UMD/CDN
* [transition between vega specs](../tests/v3/umd/transition.html) using UMD/CDN
### vega-morphcharts
* [vega spec test](../tests/v4/es6/vega-morphcharts-test-es6.html) using bundled es6
## SandDance
* [simple scatterplot](../tests/v3/umd/test.html) using UMD/CDN
@ -27,4 +23,8 @@ title: Examples
* [titanic treemap](../tests/v3/umd/treeMapTest.html) using UMD/CDN
## SandDance Explorer
* [Embed the hosted SandDance Explorer via Iframe](../tests/v3/umd/embed.html)
* [Embed the hosted SandDance Explorer via Iframe](../tests/v3/umd/embed.html)
## Previous versions
[version 3](v3)

27
docs/examples/v3.md Normal file
Просмотреть файл

@ -0,0 +1,27 @@
---
layout: page
title: V3 Examples
---
# V3 Examples
## Subsystem
### cube-layer
* [cubeTest](../tests/v3/umd/cubeTest.html) using UMD/CDN
### vega-deck.gl
* [simple vega spec](../tests/v3/umd/vega-deck.gl.test.html) using UMD/CDN
* [transition between vega specs](../tests/v3/umd/transition.html) using UMD/CDN
## SandDance
* [simple scatterplot](../tests/v3/umd/test.html) using UMD/CDN
* [scatterplot fetch + transform](../tests/v3/umd/transforms.html) using UMD/CDN
* [scatterplot](../tests/v3/es6/sanddance-test-es6.html) using bundled ES6
* [demovote scatterplot](../tests/v3/umd/scatterplotTest.html) using UMD/CDN
* [qualitative barchart](../tests/v3/umd/qualBarChartTest.html) using UMD/CDN
* [quantitative barchart](../tests/v3/umd/quanBarChartTest.html) using UMD/CDN
* [titanic treemap](../tests/v3/umd/treeMapTest.html) using UMD/CDN
## SandDance Explorer
* [Embed the hosted SandDance Explorer via Iframe](../tests/v3/umd/embed.html)

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,95 @@
html,
body {
font-family: sans-serif;
height: 100%;
}
body.rows {
display: grid;
grid-template-rows: 100px auto 1em;
grid-template-areas: "header" "view" "footer";
margin: 0;
overflow: scroll;
}
body.columns {
display: grid;
grid-template-rows: 100px auto 1em;
grid-template-columns: 40% 60%;
grid-template-areas: "header header" "left right" "footer footer";
margin: 0;
}
header {
border-bottom: 1px solid silver;
grid-area: header;
padding: 0 2em;
}
footer {
border-top: 1px solid silver;
grid-area: footer;
padding: 0 2em;
}
#view-type-button {
position: absolute;
right: 2em;
top: 2em;
}
#view-div, #view-div .vega-morphcharts-gl {
height: 100%;
}
#split-left {
grid-area: left;
overflow: hidden;
}
#vis {
grid-area: right;
position: relative;
}
.textform {
background-color: #ccc;
display: grid;
grid-template-rows: auto 2em;
height: 100%;
}
#split-left textarea {
border: 0;
height: 100%;
padding: 0 0 0 5px;
resize: none;
}
#split-right,
#error {
grid-area: right;
}
#error {
font-size: larger;
padding: 1em;
}
#split-right .vega-bindings {
padding: 1em;
}
.vega-morphcharts-root {
display: grid;
grid-template-rows: auto 200px;
height: 100%;
}
.vega-morphcharts-legend {
font-family: sans-serif;
position: absolute;
right: 1em;
top: 1em;
z-index: 1;
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>vega-morphcharts test</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/vega-morphcharts-test-es6.css" />
</head>
<body class="columns">
<header>
<h1>vega-morphcharts spec editor</h1>
<button id="view-type-button" onclick="vegaTest.specRenderer.toggleView()">2D / 3D</button>
</header>
<div id="split-left">
<div class="textform">
<textarea>
{
"$schema": "https://vega.github.io/schema/vega/v4.json",
"width": 500,
"height": 200,
"padding": 5,
"data": [
{
"name": "table",
"values": [
{ "x": 0, "y": 28, "c": 0 }, { "x": 0, "y": 55, "c": 1 },
{ "x": 1, "y": 43, "c": 0 }, { "x": 1, "y": 91, "c": 1 },
{ "x": 2, "y": 81, "c": 0 }, { "x": 2, "y": 53, "c": 1 },
{ "x": 3, "y": 19, "c": 0 }, { "x": 3, "y": 87, "c": 1 },
{ "x": 4, "y": 52, "c": 0 }, { "x": 4, "y": 48, "c": 1 },
{ "x": 5, "y": 24, "c": 0 }, { "x": 5, "y": 49, "c": 1 },
{ "x": 6, "y": 87, "c": 0 }, { "x": 6, "y": 66, "c": 1 },
{ "x": 7, "y": 17, "c": 0 }, { "x": 7, "y": 27, "c": 1 },
{ "x": 8, "y": 68, "c": 0 }, { "x": 8, "y": 16, "c": 1 },
{ "x": 9, "y": 49, "c": 0 }, { "x": 9, "y": 15, "c": 1 }
],
"transform": [
{
"type": "stack",
"groupby": ["x"],
"sort": { "field": "c" },
"field": "y"
}
]
}
],
"scales": [
{
"name": "x",
"type": "band",
"range": "width",
"domain": { "data": "table", "field": "x" }
},
{
"name": "y",
"type": "linear",
"range": "height",
"nice": true, "zero": true,
"domain": { "data": "table", "field": "y1" }
},
{
"name": "color",
"type": "ordinal",
"range": "category",
"domain": { "data": "table", "field": "c" }
}
],
"axes": [
{ "orient": "bottom", "scale": "x", "title": "X Axis", "tickColor": "red", "tickWidth": 3, "labelColor": "blue", "titleColor": "green" },
{ "orient": "left", "scale": "y", "title": "Y Axis", "domainColor": "magenta", "domainWidth": 2, "tickWidth": 7 }
],
"marks": [
{
"type": "rect",
"from": { "data": "table" },
"encode": {
"enter": {
"x": { "scale": "x", "field": "x" },
"width": { "scale": "x", "band": 1, "offset": -1 },
"y": { "scale": "y", "field": "y0" },
"y2": { "scale": "y", "field": "y1" },
"fill": { "scale": "color", "field": "c" }
},
"update": {
"fillOpacity": { "value": 1 }
},
"hover": {
"fillOpacity": { "value": 0.5 }
}
}
}
],
"legends": [
{
"fill": "color",
"title": "Legend",
"encode": {
"symbols": {
"update": {
"shape": { "value": "square" }
}
}
}
}
]
}
</textarea>
<button onclick="vegaTest.specRenderer.getText()">Apply this spec</button>
</div>
</div>
<div id="vis"></div>
<div id="error"></div>
<script type="module" src="js/vega-morphcharts-test-es6.js"></script>
</body>
</html>

Просмотреть файл

@ -3,5 +3,8 @@
"moduleResolution": "node",
"skipLibCheck": true,
"target": "es6"
}
},
"include": [
"src"
]
}

1315
packages/vega-morphcharts-test-es6/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,24 @@
{
"name": "vega-morphcharts-test-es6",
"private": true,
"version": "1.0.0",
"scripts": {
"eslint": "eslint -c ../../.eslintrc.json --fix ./src/**/*.ts*",
"build:sanddance-core": "tsc -p .",
"deploy": "parcel build ./src/vega-morphcharts-test-es6.ts"
},
"umd": "../../docs/tests/v3/es6/js/vega-morphcharts-test-es6.js",
"targets": {
"umd": {
"distDir": "../../docs/tests/v4/es6/js",
"includeNodeModules": {},
"optimize": false,
"scopeHoist": false,
"sourceMap": false
}
},
"dependencies": {
"@msrvida/vega-morphcharts": "^1",
"vega": "5.20"
}
}

Просмотреть файл

@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import * as vega from 'vega';
import * as VegaMorphCharts from '@msrvida/vega-morphcharts';
VegaMorphCharts.use(vega);
class SpecRenderer {
viewType = '2d';
spec = null;
view = null;
constructor() {
const json = localStorage.getItem('spec');
if (json) {
this.getTextArea().value = json;
}
}
public toggleView() {
if (this.viewType === '3d') {
this.viewType = '2d';
} else {
this.viewType = '3d';
}
this.getText();
}
public getTextArea() {
return <HTMLTextAreaElement>document.getElementsByTagName('textarea')[0];
}
public getText() {
var textarea = this.getTextArea();
var text = textarea.value;
var errorDiv = document.getElementById('error');
var splitRight = document.getElementById('vis');
try {
var spec = JSON.parse(text);
splitRight.style.opacity = '1';
errorDiv.style.display = 'none';
this.update(spec, text);
}
catch (e) {
errorDiv.innerText = e;
errorDiv.style.display = '';
splitRight.style.opacity = '0.1';
}
}
public update(spec: any, json: string) {
// stash the view
if (this.view != null) {
//const deckglviewstate = this.view.presenter.deckgl.viewState;
}
const runtime = vega.parse(spec);
//save in local storage
localStorage.setItem('spec', json);
this.view = new VegaMorphCharts.ViewGl(
runtime,
{
getView: () => {
return this.viewType as any
},
presenterConfig: {
onTargetViewState: (height, width) => {
return { height, width, newViewStateTarget: false };
}
}
})
.renderer('morphcharts')
.initialize(document.querySelector('#vis'));
this.view.run();
}
}
const specRenderer = new SpecRenderer();
window['vegaTest'] = {
vega,
specRenderer,
};

Просмотреть файл

@ -0,0 +1,10 @@
{
"compilerOptions": {
"moduleResolution": "node",
"skipLibCheck": true,
"target": "es6"
},
"include": [
"src"
]
}

Просмотреть файл

@ -0,0 +1,13 @@
# @msrvida/vega-morphcharts
View component for [Vega](https://vega.github.io/) visualizations, using MorphCharts for WebGL rendering.
This project combines two great visualization libraries into one. You have the expressiveness of [Vega specifications](https://vega.github.io/vega/docs/specification/) and the WebGL rendering of MorphCharts. As a result, you have the option of visualizing data in 3 dimensions.
## Limitations
This project does not fully implement every feature provided by Vega. Some interactive features are omitted due to the nature of the 3D rendering model which breaks correspondence to the 2D rendering plane. Other features simply have yet to be developed, for these we will gladly accept a pull request.
## Feature additions
Rect elements can be rendered as 3D cuboids. To do this, add `"z"` / `"depth"` encodings where you normally use `"x"` / `"width"` and `"y"` / `"height"`.

1687
packages/vega-morphcharts/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,38 @@
{
"name": "@msrvida/vega-morphcharts",
"version": "1.0.0",
"description": "MorphCharts renderer for Vega",
"main": "dist/es6/index.js",
"files": [
"dist"
],
"scripts": {
"vegatest": "parcel serve ./test/vegaspec/vega-morphcharts.test.html --open --no-hmr --no-autoinstall --no-cache",
"eslint": "eslint -c ../../.eslintrc.json --fix ./src/**/*.ts*",
"watch-typescript": "tsc -p . -w",
"build-typescript": "tsc -p .",
"build:sanddance-core": "npm run build-typescript"
},
"keywords": [
"vega",
"webgl"
],
"author": "Dan Marshall",
"license": "MIT",
"dependencies": {
"@msrvida/chart-types": "^1",
"morphcharts": "^1",
"d3-color": "^1.4.0",
"d3-ease": "^1.0.5",
"deepmerge": "^2.1.1",
"is-plain-object": "^5.0.0",
"tsx-create-element": "^2.2.5",
"vega-typings": "0.21.0"
},
"devDependencies": {
"@types/d3-ease": "^3.0.0",
"@types/react": ">=16.8.0 <18.0.0",
"typescript": "^4.2.3",
"vega": "5.20.2"
}
}

Просмотреть файл

@ -0,0 +1,25 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
export function concat<T>(...args: T[][]) {
return args.reduce((p, c) => c ? p.concat(c) : p, []);
}
/**
* Returns array with items which are truthy.
* @param args array or arrays to concat into a single array.
*/
export function allTruthy<T>(...args: T[][]) {
return args.reduce((p, c) => c ? p.concat(c) : p, []).filter(Boolean);
}
/**
* Add an array to an existing array in place.
* @param arr Array to append to.
* @param items Arrof of items to append.
*/
export function push<T>(arr: T[], items: T[]) {
arr.push.apply(arr, items);
}

Просмотреть файл

@ -0,0 +1,74 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import {
CanvasHandler,
inferType,
inferTypes,
loader,
parse,
read,
Renderer,
renderModule,
sceneVisit,
scheme,
truncate,
View,
} from 'vega-typings';
/**
* Vega library dependency.
*/
export interface VegaBase {
CanvasHandler: CanvasHandler;
inferType: typeof inferType;
inferTypes: typeof inferTypes;
loader: typeof loader;
parse: typeof parse;
read: typeof read;
renderModule: typeof renderModule;
Renderer: typeof Renderer,
sceneVisit: typeof sceneVisit
scheme: typeof scheme,
truncate: typeof truncate,
View: typeof View,
}
const vega: VegaBase = {
CanvasHandler: null,
inferType: null,
inferTypes: null,
loader: null,
parse: null,
read: null,
renderModule: null,
Renderer: null,
sceneVisit: null,
scheme: null,
truncate: null,
View: null,
};
/**
* References to dependency libraries.
*/
export interface Base {
vega: VegaBase;
}
/**
* References to dependency libraries.
*/
export const base: Base = {
vega,
};
/**
* Specify the dependency libraries to use for rendering.
* @param vega Vega library.
*/
export function use(vega: VegaBase) {
base.vega = vega;
}

Просмотреть файл

@ -0,0 +1,21 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import * as _deepmerge from 'deepmerge';
import { isPlainObject } from 'is-plain-object';
const deepmerge = ((_deepmerge as any).default || _deepmerge) as typeof _deepmerge;
export function clone<T extends object>(objectToClone: T) {
if (!objectToClone) return objectToClone;
return deepmerge.all([objectToClone]) as T;
}
const dontMerge = (destination, source) => source;
export function deepMerge<T>(...objectsToMerge: T[]) {
const objects = objectsToMerge.filter(Boolean) as any as object[];
return deepmerge.all(objects, { arrayMerge: dontMerge, isMergeableObject: isPlainObject }) as any as T;
}

Просмотреть файл

@ -0,0 +1,64 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import {
color as d3color,
hsl as d3hsl,
rgb as d3rgb,
RGBColor,
} from 'd3-color';
import { RGBAColor } from './interfaces';
function rgbToDeckglColor(c: RGBColor): RGBAColor {
return [c.r, c.g, c.b, c.opacity * 255];
}
/**
* Compares 2 colors to see if they are equal.
* @param a RGBAColor to compare
* @param b RGBAColor to compare
* @returns True if colors are equal.
*/
export function colorIsEqual(a: RGBAColor, b: RGBAColor) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
/**
* Convert a CSS color string to a Deck.gl RGBAColor array - (The rgba color of each object, in r, g, b, [a]. Each component is in the 0-255 range.).
* @param cssColorSpecifier A CSS Color Module Level 3 specifier string.
*/
export function colorFromString(cssColorSpecifier: string): RGBAColor {
if (cssColorSpecifier) {
const dc = d3color(cssColorSpecifier);
if (dc) {
const c = dc.rgb();
return rgbToDeckglColor(c);
}
}
}
/**
* Convert a Deck.gl color to a CSS rgba() string.
* @param color A Deck.gl RGBAColor array - (The rgba color of each object, in r, g, b, [a]. Each component is in the 0-255 range.)
*/
export function colorToString(color: RGBAColor) {
const c = [...color];
if (c.length > 3) {
c[3] /= 255;
}
return `rgba(${c.join(',')})`;
}
export function desaturate(color: RGBAColor, value: number): RGBAColor {
const rgb = d3rgb(color[0], color[1], color[2], color[3] / 255);
const hslColor = d3hsl(rgb);
hslColor.s = value;
const c = hslColor.rgb();
return rgbToDeckglColor(c);
}

Просмотреть файл

@ -0,0 +1,54 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { createElement, StatelessComponent, StatelessProps } from 'tsx-create-element';
export interface TableCell {
className?: string;
content: string | JSX.Element;
title?: string;
}
export interface TableRow {
cells: TableCell[];
}
export interface TableProps {
className?: string;
onRowClick?: (e: Event, index: number) => void;
rows: TableRow[];
rowClassName?: string;
}
const KeyCodes = {
ENTER: 'Enter',
};
export const Table: StatelessComponent<TableProps> = (props: StatelessProps<TableProps>) => {
return (
<table className={props.className}>
{props.children}
{props.rows.map((row, i) => (
<tr
className={props.rowClassName || ''}
onClick={e => props.onRowClick && props.onRowClick(e as any as MouseEvent, i)}
tabIndex={props.onRowClick ? 0 : -1}
onKeyUp={e => {
if (e.key === KeyCodes.ENTER && props.onRowClick) {
props.onRowClick(e as any as KeyboardEvent, i);
}
}}
>
{row.cells.map((cell, i) => (
<td
className={cell.className || ''}
title={cell.title || ''}
>{cell.content}</td>
))}
</tr>
))}
</table>
);
};

Просмотреть файл

@ -0,0 +1,106 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import {
Axis,
Cube,
PresenterConfig,
PresenterStyle,
RGBAColor,
Stage,
StyledLine,
VegaTextLayerDatum,
} from './interfaces';
import { View } from '@msrvida/chart-types';
import { SceneLine, SceneText } from 'vega-typings/types/runtime/scene';
import { colorIsEqual } from './color';
export const minHeight = '100px';
export const minWidth = '100px';
export const defaultPresenterStyle: PresenterStyle = {
cssPrefix: 'vega-morphcharts-',
defaultCubeColor: [128, 128, 128, 255],
highlightColor: [0, 0, 0, 255],
};
export const defaultPresenterConfig: PresenterConfig = {
onCubeClick: (e, cube: Cube) => { },
onCubeHover: (e, cube: Cube) => { },
transitionDurations: {
color: 100,
position: 600,
stagger: 600,
view: 600,
},
initialMcRendererOptions: {
advanced: false,
advancedOptions: {},
basicOptions: {
antialias: true,
},
},
};
export function createStage(view: View) {
const stage: Stage = {
view,
cubeData: [],
pathData: [],
axes: {
x: [],
y: [],
z: [],
},
gridLines: [],
textData: [],
legend: {
rows: {},
},
facets: [],
};
return stage;
}
export const groupStrokeWidth = 1;
export const lineZ = 0;
export const defaultView: View = '2d';
export const minZ = 0.5;
export const min3dDepth = 0.05;
export const minPixelSize = 0.5;
const zAxisEncodeColor: RGBAColor = [7, 7, 7, 255];
const zAxisOutColor: RGBAColor = [0, 0, 0, 255];
export function defaultOnAxisItem(vegaItem: SceneLine | SceneText, stageItem: StyledLine | VegaTextLayerDatum, stage: Stage, currAxis: Axis) {
if (colorIsEqual(stageItem.color, zAxisEncodeColor)) {
stageItem.color = zAxisOutColor;
if (currAxis.axisRole !== 'z') {
const previousAxisRole = removeCurrentAxes(stage, currAxis);
if (previousAxisRole) {
currAxis.axisRole = 'z';
stage.axes.z.push(currAxis);
} else {
//debug: curr axis not found
}
}
}
}
function removeCurrentAxes(stage: Stage, currAxis: Axis) {
//find the current axis, remove it from parent
for (const axisRole in stage.axes) {
const axes: Axis[] = stage.axes[axisRole];
for (let i = 0; i < axes.length; i++) {
if (axes[i] === currAxis) {
axes.splice(i, 1);
return axisRole;
}
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { easeCubicInOut } from 'd3-ease';
export function easing(t: number) {
if (t === 0 || t === 1) return t;
return easeCubicInOut(t);
}

Просмотреть файл

@ -0,0 +1,11 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
/**
* HTML elements outputted by the presenter.
*/
export enum PresenterElement {
root, gl, panel, legend, vegaControls,
}

Просмотреть файл

@ -0,0 +1,17 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
/**
* This file is for external facing export only, do not use this for internal references,
* as it may cause circular dependencies in Rollup.
*/
import { Table, TableCell, TableProps, TableRow } from '../controls';
//alphabetize interfaces for documentation
export { TableCell, TableProps, TableRow };
//alphabetize variables for documentation
export { Table };

Просмотреть файл

@ -0,0 +1,72 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
/**
* This file is for external facing export only, do not use this for internal references,
* as it may cause circular dependencies in Rollup.
*/
import {
Axis,
AxisRole,
Cube,
FacetRect,
Legend,
LegendRow,
LegendRowSymbol,
McColor,
McColors,
MorphChartsCore,
MorphChartsColorMapper,
PresenterConfig,
PresenterStyle,
PreStage,
QueuedAnimationOptions,
Scene3d,
Stage,
StyledLine,
TickText,
TransitionDurations,
UnitColorMap,
Vec3,
VegaTextLayerDatum,
} from '../interfaces';
import {
Base,
VegaBase,
} from '../base';
import { ViewGlConfig } from '../vega-classes/viewGl';
//alphabetize interfaces for documentation
export {
Axis,
AxisRole,
Base,
Cube,
FacetRect,
Legend,
LegendRow,
LegendRowSymbol,
McColor,
McColors,
MorphChartsCore,
MorphChartsColorMapper,
PreStage,
PresenterConfig,
PresenterStyle,
QueuedAnimationOptions,
Scene3d,
Stage,
StyledLine,
TickText,
TransitionDurations,
UnitColorMap,
VegaBase,
VegaTextLayerDatum,
ViewGlConfig,
};
//alphabetize types for documentation
export { Vec3 };

Просмотреть файл

@ -0,0 +1,18 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
/**
* This file is for external facing export only, do not use this for internal references,
* as it may cause circular dependencies in Rollup.
*/
import { allTruthy, concat, push } from '../array';
import { addDiv, addEl, outerSize } from '../htmlHelpers';
import { clone, deepMerge } from '../clone';
import { colorFromString, colorIsEqual, colorToString, desaturate } from '../color';
import { createElement, getActiveElementInfo, mount, setActiveElement } from 'tsx-create-element';
//alphabetize for documentation
export { addDiv, addEl, allTruthy, clone, colorFromString, colorIsEqual, colorToString, concat, createElement, deepMerge, desaturate, getActiveElementInfo, mount, outerSize, push, setActiveElement };

Просмотреть файл

@ -0,0 +1,40 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
/**
* Create a new element as a child of another element.
* @param tagName Tag name of the new tag to create.
* @param parentElement Reference of the element to append to.
* @returns new HTMLElement.
*/
export function addEl(tagName: string, parentElement: HTMLElement) {
const el = document.createElement(tagName);
parentElement.appendChild(el);
return el;
}
/**
* Create a new div HTMLElement as a child of another element.
* @param parentElement Reference of the element to append to.
* @param className Optional css class name to apply to the div.
*/
export function addDiv(parentElement: HTMLElement, className?: string) {
const div = addEl('div', parentElement) as HTMLDivElement;
if (className) {
div.className = className;
}
return div;
}
/**
* Measure the outer height and width of an HTMLElement, including margin, padding and border.
* @param el HTML Element to measure.
*/
export function outerSize(el: HTMLElement) {
const cs = getComputedStyle(el);
const height = parseFloat(cs.marginTop) + parseFloat(cs.paddingTop) + parseFloat(cs.borderTopWidth) + el.offsetHeight + parseFloat(cs.borderBottomWidth) + parseFloat(cs.paddingBottom) + parseFloat(cs.marginBottom);
const width = parseFloat(cs.marginLeft) + parseFloat(cs.paddingLeft) + parseFloat(cs.borderLeftWidth) + el.offsetWidth + parseFloat(cs.borderRightWidth) + parseFloat(cs.paddingRight) + parseFloat(cs.marginRight);
return { height, width };
}

Просмотреть файл

@ -0,0 +1,20 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import * as controls from './exports/controls';
import * as types from './exports/types';
import * as util from './exports/util';
import * as defaults from './defaults';
export { base, use } from './base';
export { Presenter } from './presenter';
export { ViewGl } from './vega-classes/viewGl';
export * from './enums';
export { controls, defaults, types, util };
export { McRendererOptions, RGBAColor } from './interfaces';
export { version } from './version';

Просмотреть файл

@ -0,0 +1,331 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { Camera, View } from '@msrvida/chart-types';
import { Scene, SceneLine, SceneText } from 'vega-typings';
import { Axes, Core, Renderers } from 'morphcharts';
import { Config } from 'morphcharts/dist/renderers/advanced/config';
import { quat, vec3 } from 'gl-matrix';
export type Position = [number, number, number];
export type RGBAColor = [number, number, number, number];
export { Core as MorphChartsCore };
export interface VegaTextLayerDatum {
color: RGBAColor;
text: string;
position: Position;
size: number;
angle?: number;
//textAnchor?: TextAnchor; //TODO
//alignmentBaseline?: AlignmentBaseline;
metaData?: any;
}
export interface StyledLine {
color?: RGBAColor;
sourcePosition: Vec3;
strokeWidth?: number;
targetPosition: Vec3;
}
export interface TickText extends VegaTextLayerDatum {
value: number | string;
}
export type AxisRole = 'x' | 'y' | 'z';
export interface Axis {
axisRole: AxisRole;
domain: StyledLine;
ticks: StyledLine[];
tickText: TickText[];
title?: VegaTextLayerDatum;
}
/**
* 3 dimensional array of numbers.
*/
export type Vec3 = [number, number, number];
/**
* Cuboid information. The cube does not need to have equal dimensions.
*/
export interface Cube {
/**
* Ordinal position.
*/
ordinal?: number;
/**
* Flag whether this cube is a "placeholder" and is not to be rendered nor contains cube data.
*/
isEmpty?: boolean;
color: RGBAColor;
position: Vec3;
size: Vec3;
}
export interface Path {
positions: Position[];
strokeColor: RGBAColor;
strokeWidth: number;
}
export interface ImageBounds {
x1: number;
y1: number;
x2: number;
y2: number;
}
export interface Image {
bounds: ImageBounds;
height: number;
url: string;
width: number;
}
/**
* Vega Scene plus camera type.
*/
export interface Scene3d extends Scene {
view: View;
}
/**
* Rect area and title for a facet.
*/
export interface FacetRect {
datum: any;
lines: StyledLine[];
}
/**
* Data structure containing all that is necessary to present a chart.
*/
export interface Stage {
redraw?: boolean;
backgroundColor?: RGBAColor;
backgroundImages?: Image[];
cubeData?: Cube[];
pathData?: Path[];
legend?: Legend;
axes?: { [key in AxisRole]: Axis[] };
textData?: VegaTextLayerDatum[];
view?: View;
gridLines?: StyledLine[];
facets?: FacetRect[];
}
export interface Legend {
title?: string;
rows: { [index: number]: LegendRow };
}
export interface LegendRow {
label?: string;
value?: string;
symbol?: LegendRowSymbol;
}
export interface LegendRowSymbol {
bounds: {
x1: number;
y1: number;
x2: number;
y2: number;
};
fill: string;
shape: string;
}
/**
* Function that can be called prior to presenting the stage.
*/
export interface PreStage {
(stage: Stage, colorMapper: MorphChartsColorMapper): void;
}
/**
* Lengths of time for a transition animation.
*/
export interface TransitionDurations {
color?: number;
position?: number;
stagger?: number;
view?: number;
}
/**
* Configuration options to be used by the Presenter.
*/
export interface PresenterConfig {
getCameraTo?: () => Camera;
transitionDurations?: TransitionDurations;
mcColors?: McColors;
initialMcRendererOptions?: McRendererOptions;
preStage?: PreStage;
getCharacterSet?: (stage: Stage) => string[];
redraw?: () => void;
onCubeHover?: (e: MouseEvent | PointerEvent | TouchEvent, cube: Cube) => void;
onCubeClick?: (e: MouseEvent | PointerEvent | TouchEvent, cube: Cube) => void;
onLayerClick?: (e: MouseEvent) => any;
onLasso?: (ids: Set<number>, e: MouseEvent | PointerEvent | TouchEvent) => void;
onLegendClick?: (e: MouseEvent | PointerEvent | TouchEvent, legend: Legend, clickedIndex: number) => void;
onPresent?: () => void;
shouldViewstateTransition?: () => boolean;
preLayer?: (stage: Stage) => void;
onTextClick?: (e: MouseEvent | PointerEvent | TouchEvent, t: VegaTextLayerDatum) => void;
onTextHover?: (e: MouseEvent | PointerEvent | TouchEvent, t: VegaTextLayerDatum) => boolean;
getTextColor?: (o: VegaTextLayerDatum) => RGBAColor;
getTextHighlightColor?: (o: VegaTextLayerDatum) => RGBAColor;
onSceneRectAssignCubeOrdinal?: (d: object) => number | undefined;
onAxisItem?: (vegaItem: SceneLine | SceneText, stageItem: StyledLine | VegaTextLayerDatum, stage: Stage, currAxis: Axis) => void;
onAxisConfig?: (cartesian: Axes.Cartesian2dAxes | Axes.Cartesian3dAxes, dim3d: number, axis: Axis) => void;
onAxesComplete?: (cartesian: Axes.Cartesian2dAxes | Axes.Cartesian3dAxes) => void;
axisPickGridCallback?: (divisions: number[], e: MouseEvent | PointerEvent | TouchEvent) => void;
onTargetViewState?: (height: number, width: number) => { height: number, width: number, newViewStateTarget?: boolean };
preserveDrawingBuffer?: boolean;
zAxisZindex?: number;
layerSelection?: LayerSelection;
}
export interface PresenterStyle {
cssPrefix?: string;
defaultCubeColor?: RGBAColor;
highlightColor?: RGBAColor;
fontFamily?: string;
}
/**
* Options to pass to Presenter.queueAnimation()
*/
export interface QueuedAnimationOptions {
/**
* Debug label to observe which animation is waiting.
*/
waitingLabel?: string;
/**
* Debug label to observe which handler is invoked.
*/
handlerLabel?: string;
/**
* Function to invoke if animation was interrupted when another animation is queued.
*/
animationCanceled?: () => void;
}
export interface UnitColorMap {
ids: Uint32Array;
minColor: number;
maxColor: number;
colors: Float64Array;
palette: Uint8Array;
}
/**
* MorphCharts interfaces
*/
export interface AdvancedRendererOptions extends Partial<Config> { }
export interface BasicRendererOptions extends Renderers.Basic.IRendererOptions { }
export interface McRendererOptions {
advanced: boolean;
advancedOptions: AdvancedRendererOptions;
basicOptions: BasicRendererOptions;
}
export interface MorphChartsRef {
reset: () => void;
core: Core;
isCameraMovement: boolean;
isTransitioning: boolean;
cameraTime: number;
transitionTime: number;
transitionModel: boolean;
setMcRendererOptions: (value: McRendererOptions) => void;
lastMcRendererOptions: McRendererOptions;
qModelFrom: quat;
qModelTo: quat;
qModelCurrent: quat;
qCameraRotationFrom: quat;
qCameraRotationTo: quat;
qCameraRotationCurrent: quat;
vCameraPositionFrom: vec3;
vCameraPositionTo: vec3;
vCameraPositionCurrent: vec3;
supportedRenders: {
advanced: boolean;
basic: boolean;
}
}
export interface ILayerProps {
ref: MorphChartsRef;
stage: Stage;
height: number;
width: number;
bounds?: IBounds;
config: PresenterConfig;
}
export interface IBounds {
minBoundsX: number;
minBoundsY: number;
minBoundsZ: number;
maxBoundsX: number;
maxBoundsY: number;
maxBoundsZ: number;
}
export interface ILayer {
bounds: IBounds;
update?: (bounds: IBounds, selected?: Set<number>) => void;
unitColorMap?: UnitColorMap
}
export type ILayerCreator = (props: ILayerProps) => ILayer;
export interface LayerSelection {
cubes?: Set<number>;
lines?: Set<number>;
texts?: Set<number>;
}
export interface MorphChartsColorMapper {
getCubeUnitColorMap: () => UnitColorMap;
setCubeUnitColorMap: (unitColorMap: UnitColorMap) => void;
}
export interface MorphChartsRendering extends MorphChartsColorMapper {
activate(id: number),
update: (layerSelection: LayerSelection) => void;
moveCamera: (position: vec3, rotation: quat) => void;
}
export type McColor = [number, number, number];
export interface McColors {
activeItemColor: string;
backgroundColor: string;
textColor: string;
textBorderColor: string;
axesTextLabelColor: string;
axesTextTitleColor: string;
axesTextHeadingColor: string;
axesGridBackgroundColor: string;
axesGridHighlightColor: string;
axesGridMinorColor: string;
axesGridMajorColor: string;
axesGridZeroColor: string;
}

Просмотреть файл

@ -0,0 +1,64 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { createElement, StatelessComponent, StatelessProps } from 'tsx-create-element';
import { Legend, LegendRow, LegendRowSymbol } from './interfaces';
import { Table, TableRow } from './controls';
export interface LegendViewProps {
legend: Legend;
onClick: (e: Event, legend: Legend, clickedIndex: number) => void;
}
export const LegendView: StatelessComponent<LegendViewProps> = (props: StatelessProps<LegendViewProps>) => {
const rows: TableRow[] = [];
const addRow = (row: LegendRow, i: number) => {
const fn = symbolMap[row.symbol.shape];
let jsx: JSX.Element;
if (fn) {
jsx = fn(row.symbol);
} else {
jsx = <span>x</span>;
//console.log(`need to render ${row.symbol.shape} symbol shape`);
}
rows.push({
cells: [
{ className: 'symbol', content: jsx },
{ className: 'label', content: row.label, title: row.label },
],
});
};
const sorted = Object.keys(props.legend.rows).sort((a, b) => +a - +b);
sorted.forEach(i => addRow(props.legend.rows[i], +i));
if (sorted.length) {
return (
<Table
rows={rows}
rowClassName="legend-row"
onRowClick={(e, i) => props.onClick(e, props.legend, i)}
>
{props.legend.title !== void 0 && <tr onClick={e => props.onClick(e as any as MouseEvent, props.legend, null)} ><th colSpan={2}>{props.legend.title}</th></tr>}
</Table>
);
}
};
const symbolMap: { [shape: string]: (symbol: LegendRowSymbol) => JSX.Element } = {
square: function (symbol: LegendRowSymbol) {
return (
<div style={{
height: `${symbol.bounds.y2 - symbol.bounds.y1}px`,
width: `${symbol.bounds.x2 - symbol.bounds.x1}px`,
backgroundColor: symbol.fill,
borderColor: symbol.fill,
}} />
);
},
};

Просмотреть файл

@ -0,0 +1,32 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { base } from '../base';
import { Stage } from '../interfaces';
import { Scene, SceneItem } from 'vega-typings';
import { GroupType, MarkStager, MarkStagerOptions } from './interfaces';
interface SceneImage extends SceneItem {
height: number;
url: string;
width: number;
}
const markStager: MarkStager = (options: MarkStagerOptions, stage: Stage, scene: Scene, x: number, y: number, groupType: GroupType) => {
base.vega.sceneVisit(scene, function (item: SceneImage) {
const { bounds, height, url, width } = item;
let { x1, x2, y1, y2 } = bounds;
x1 += x;
x2 += x;
y1 += y;
y2 += y;
if (!stage.backgroundImages) {
stage.backgroundImages = [];
}
stage.backgroundImages.push({ bounds: { x1, x2, y1, y2 }, height, url, width });
});
};
export default markStager;

Просмотреть файл

@ -0,0 +1,32 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { Axis, RGBAColor, Stage, StyledLine, VegaTextLayerDatum } from '../interfaces';
import { Scene, SceneLine, SceneText } from 'vega-typings';
export enum GroupType {
none = 0,
legend = 1,
xAxis = 2,
yAxis = 3,
zAxis = 4,
}
export interface MarkStagerOptions {
maxOrdinal: number;
currAxis: Axis;
defaultCubeColor: RGBAColor;
assignCubeOrdinal: (d: object) => number | undefined;
modifyAxis?: (vegaItem: SceneLine | SceneText, stageItem: StyledLine | VegaTextLayerDatum, stage: Stage, currAxis: Axis) => void;
zAxisZindex: number;
}
export interface LabelDatum {
value: any;
}
export interface MarkStager {
(options: MarkStagerOptions, stage: Stage, scene: Scene, x: number, y: number, groupType: GroupType): void;
}

Просмотреть файл

@ -0,0 +1,58 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { base } from '../base';
import {
GroupType,
LabelDatum,
MarkStager,
MarkStagerOptions,
} from './interfaces';
import { Legend, LegendRowSymbol, Stage } from '../interfaces';
import {
Scene,
SceneItem,
SceneLegendItem,
SceneSymbol,
SceneText,
} from 'vega-typings';
const legendMap: { [role: string]: (legend: Legend, item: SceneItem) => void } = {
'legend-title': function (legend: Legend, textItem: SceneText) {
legend.title = textItem.text;
},
'legend-symbol': function (legend: Legend, symbol: SceneSymbol & SceneLegendItem) {
const { bounds, fill, shape } = symbol;
//this object is safe for serialization
const legendRowSymbol: LegendRowSymbol = { bounds, fill, shape };
const i = symbol.datum.index;
legend.rows[i] = legend.rows[i] || {};
legend.rows[i].symbol = legendRowSymbol;
},
'legend-label': function (legend: Legend, label: SceneText & SceneLegendItem) {
const i = label.datum.index;
legend.rows[i] = legend.rows[i] || {};
const row = legend.rows[i];
row.label = label.text;
row.value = (label.datum as unknown as LabelDatum).value;
},
};
const markStager: MarkStager = (options: MarkStagerOptions, stage: Stage, scene: Scene, x: number, y: number, groupType: GroupType) => {
base.vega.sceneVisit(scene, function (item: SceneItem) {
const fn = legendMap[item.mark.role];
if (fn) {
fn(stage.legend, item);
} else {
//console.log(`need to render legend ${item.mark.role}`);
}
});
};
export default markStager;

Просмотреть файл

@ -0,0 +1,49 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { GroupType, MarkStager, MarkStagerOptions } from './interfaces';
import { colorFromString } from '../color';
import { Path, Stage } from '../interfaces';
import { Datum, Scene, SceneGroup } from 'vega-typings';
type GroupItem = SceneGroup & {
datum: Datum;
length: number;
depth: number;
opacity: number;
z: number;
strokeWidth: number,
strokeOpacity: number;
}
//change direction of y from SVG to GL
const ty = -1;
const markStager: MarkStager = (options: MarkStagerOptions, stage: Stage, scene: Scene, x: number, y: number, groupType: GroupType) => {
const g: GroupItem = {
opacity: 1,
strokeOpacity: 1,
strokeWidth: 1,
...(<GroupItem>scene.items[0]),
};
const path: Path = {
strokeWidth: g.strokeWidth,
strokeColor: colorFromString(g.stroke),
positions: scene.items.map((it: GroupItem) => [
it.x,
ty * it.y,
it.z || 0,
]),
};
path.strokeColor[3] *= g.strokeOpacity;
path.strokeColor[3] *= g.opacity;
stage.pathData.push(path);
};
export default markStager;

Просмотреть файл

@ -0,0 +1,53 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { base } from '../base';
import { colorFromString } from '../color';
import { Cube, Stage } from '../interfaces';
import { Datum, Scene, SceneRect } from 'vega-typings';
import { GroupType, MarkStager, MarkStagerOptions } from './interfaces';
import { min3dDepth, minZ } from '../defaults';
type SceneCube = SceneRect & {
datum: Datum;
depth: number;
opacity: number;
z: number;
}
const markStager: MarkStager = (options: MarkStagerOptions, stage: Stage, scene: Scene, x: number, y: number, groupType: GroupType) => {
base.vega.sceneVisit(scene, function (item: SceneCube) {
const z = stage.view === '2d' ? 0 : (item.z || 0) + minZ;
const depth = (stage.view === '2d' ? 0 : (item.depth || 0)) + min3dDepth;
//change direction of y from SVG to GL
const ty = -1;
const ordinal = options.assignCubeOrdinal(item.datum);
if (ordinal > options.maxOrdinal) {
options.maxOrdinal = ordinal;
}
if (ordinal === undefined) {
//TODO add to polygons
//console.log('not a cube');
} else {
const cube: Cube = {
ordinal,
size: [item.width, item.height, depth],
position: [x + ((+item.x) || 0), ty * (y + ((+item.y) || 0)) - (+item.height), z],
color: colorFromString(item.fill) || options.defaultCubeColor || [128, 128, 128, 128],
};
cube.color[3] = item.opacity === undefined ? 255 : 255 * item.opacity;
stage.cubeData.push(cube);
}
});
};
export default markStager;

Просмотреть файл

@ -0,0 +1,62 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { base } from '../base';
import { colorFromString } from '../color';
import { GroupType, MarkStager, MarkStagerOptions } from './interfaces';
import { lineZ } from '../defaults';
import { Scene, SceneLine } from 'vega-typings';
import { Stage, StyledLine } from '../interfaces';
const markStager: MarkStager = (options: MarkStagerOptions, stage: Stage, scene: Scene, x: number, y: number, groupType: GroupType) => {
base.vega.sceneVisit(scene, function (item: SceneLine) {
const x1 = item.x || 0;
const y1 = item.y || 0;
const x2 = item.x2 != null ? item.x2 : x1;
const y2 = item.y2 != null ? item.y2 : y1;
const lineItem = styledLine(x1 + x, y1 + y, x2 + x, y2 + y, item.stroke, item.strokeWidth);
const { currAxis } = options;
if (options.modifyAxis) {
options.modifyAxis(item, lineItem, stage, currAxis);
}
if (item.mark.role === 'axis-tick') {
currAxis.ticks.push(lineItem);
} else if (item.mark.role === 'axis-domain') {
currAxis.domain = lineItem;
} else {
stage.gridLines.push(lineItem);
}
});
};
function styledLine(x1: number, y1: number, x2: number, y2: number, stroke: string, strokeWidth: number) {
const line: StyledLine = {
sourcePosition: [x1, -y1, lineZ], //-1 = change direction of y from SVG to GL
targetPosition: [x2, -y2, lineZ],
color: colorFromString(stroke),
strokeWidth: strokeWidth,
};
return line;
}
export function box(gx: number, gy: number, height: number, width: number, stroke: string, strokeWidth: number, diagonals = false) {
const lines = [
styledLine(gx, gy, gx + width, gy, stroke, strokeWidth),
styledLine(gx + width, gy, gx + width, gy + height, stroke, strokeWidth),
styledLine(gx + width, gy + height, gx, gy + height, stroke, strokeWidth),
styledLine(gx, gy + height, gx, gy, stroke, strokeWidth),
];
if (diagonals) {
lines.push(styledLine(gx, gy, gx + width, gy + height, stroke, strokeWidth));
lines.push(styledLine(gx, gy + height, gx + width, gy, stroke, strokeWidth));
}
return lines;
}
export default markStager;

Просмотреть файл

@ -0,0 +1,91 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
// import { AlignmentBaseline, TextAnchor } from '@deck.gl/layers/text-layer/text-layer';
import { base } from '../base';
import { colorFromString } from '../color';
import {
GroupType,
LabelDatum,
MarkStager,
MarkStagerOptions,
} from './interfaces';
import {
Scene,
SceneText,
SceneTextAlign,
SceneTextBaseline,
} from 'vega-typings';
import { Stage, TickText, VegaTextLayerDatum } from '../interfaces';
interface SceneText2 extends SceneText {
metaData?: any;
ellipsis?: string;
limit?: number;
}
const markStager: MarkStager = (options: MarkStagerOptions, stage: Stage, scene: Scene, x: number, y: number, groupType: GroupType) => {
//change direction of y from SVG to GL
const ty = -1;
base.vega.sceneVisit(scene, function (item: SceneText2) {
if (!item.text) return;
const size = item.fontSize;
//const alignmentBaseline = convertBaseline(item.baseline);
//const yOffset = alignmentBaseline === 'top' ? item.fontSize / 2 : 0; //fixup to get tick text correct
const yOffset = 0;
const textItem: VegaTextLayerDatum = {
color: colorFromString(item.fill),
text: item.limit === undefined ? item.text : base.vega.truncate(item.text, item.limit, 'right', item.ellipsis || '...'), //use dots instead of unicode ellipsis for
position: [x + (item.x || 0), ty * (y + (item.y || 0) + yOffset), 0],
size,
angle: convertAngle(item.angle),
//textAnchor: convertAlignment(item.align),
//alignmentBaseline,
metaData: item.metaData,
};
const { currAxis } = options;
if (options.modifyAxis) {
options.modifyAxis(item, textItem, stage, currAxis);
}
if (item.mark.role === 'axis-label') {
const tickText = textItem as TickText;
tickText.value = (item.datum as LabelDatum).value;
currAxis.tickText.push(tickText);
} else if (item.mark.role === 'axis-title') {
currAxis.title = textItem;
} else {
stage.textData.push(textItem);
}
});
};
function convertAngle(vegaTextAngle: number) {
if (vegaTextAngle && !isNaN(vegaTextAngle)) {
return 360 - vegaTextAngle;
}
return 0;
}
// function convertAlignment(textAlign: SceneTextAlign): TextAnchor {
// switch (textAlign) {
// case 'center': return 'middle';
// case 'left': return 'start';
// case 'right': return 'end';
// }
// return 'start';
// }
// function convertBaseline(baseline: SceneTextBaseline): AlignmentBaseline {
// switch (baseline) {
// case 'middle': return 'center';
// }
// return baseline || 'bottom';
// }
export default markStager;

Просмотреть файл

@ -0,0 +1,392 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { Axes, AxesVisibility, AxesTextOrientation, Edge3D, Meshes } from 'morphcharts';
import { Cartesian2dAxes, Cartesian3dAxes } from 'morphcharts/dist/components/axes';
import { Axis, IBounds, ILayer, ILayerCreator, ILayerProps, StyledLine, Stage, Vec3 } from '../interfaces';
import { outerBounds } from './bounds';
export const createAxesLayer: ILayerCreator = (props: ILayerProps): ILayer => {
const { config, height, ref, stage } = props;
const { core } = ref;
const { renderer } = core;
const { x, y, z } = stage.axes;
const xyz = [...x, ...y, ...z];
renderer.currentAxes = [];
if (!xyz.length) {
renderer.axesVisibility = AxesVisibility.none;
return;
}
renderer.axesVisibility = AxesVisibility.current;
const correlation = new AxesCorrelation(stage, 3);
const { axesSets, labels } = correlation;
const grid = correlation.getGrid();
if (grid.byColumn[0]) {
grid.byColumn[0].forEach(row => { row.axesSet.showFacetTitleY = true; });
grid.byRow[0].forEach(col => { col.axesSet.showFacetTitleX = true; });
}
if (grid.rows > 1) {
const { byRow } = grid;
byRow[0].forEach(({ axesSet }, col) => {
if (!axesSet.y) {
if (byRow[1][col].axesSet) {
//move x up
byRow[1][col].axesSet.x.tickText = axesSet.x.tickText;
byRow[1][col].axesSet.showFacetTitleX = axesSet.showFacetTitleX;
delete axesSet.x;
}
}
});
}
let bounds: IBounds;
const allAxesSetBounds: AxesSetBounds[] = [];
let anyZ = false;
for (let i = 0; i < axesSets.length; i++) {
if (axesSets[i].z) {
anyZ = true;
break;
}
}
const is3d = stage.view === '3d' && anyZ;
axesSets.forEach(axesSet => {
if (!axesSet.x && !axesSet.y) return;
const axesSetBounds: AxesSetBounds = {
axesSet,
maxBoundsX: null,
maxBoundsY: null,
maxBoundsZ: null,
minBoundsX: null,
minBoundsY: null,
minBoundsZ: null,
};
if (is3d) {
const zBounds = getDomainBounds(1, axesSet.z);
axesSetBounds.minBoundsZ = -zBounds.minBounds;
axesSetBounds.maxBoundsZ = -zBounds.maxBounds;
}
const yBounds = getDomainBounds(1, axesSet.y);
axesSetBounds.minBoundsY = yBounds.minBounds;
axesSetBounds.maxBoundsY = yBounds.maxBounds;
axesSetBounds.y = yBounds.minBounds;
axesSetBounds.h = yBounds.maxBounds - yBounds.minBounds;
const xBounds = getDomainBounds(0, axesSet.x);
axesSetBounds.minBoundsX = xBounds.minBounds;
axesSetBounds.maxBoundsX = xBounds.maxBounds;
axesSetBounds.x = xBounds.minBounds;
axesSetBounds.w = xBounds.maxBounds - xBounds.minBounds;
allAxesSetBounds.push(axesSetBounds);
bounds = outerBounds(bounds, axesSetBounds);
});
const facetLabelX = labels.filter(label => label.axisRole === 'x')[0];
const facetLabelY = labels.filter(label => label.axisRole === 'y')[0];
core.inputManager.pickAxesTitleCallback = ({ axis, axes, manipulator }) => {
const axesSet = axesSets[axes];
let a: Axis;
let f: Axis;
switch (axis) {
case 0: {
a = axesSet.x;
f = facetLabelX;
break;
}
case 1: {
a = axesSet.y;
f = facetLabelY;
break;
}
case 2: {
a = axesSet.z;
break;
}
}
if (a) {
config.onTextClick(manipulator.event as MouseEvent, a.title || f.title);
}
};
allAxesSetBounds.forEach(axesSetBounds => {
const { axesSet } = axesSetBounds;
if (!axesSet.x && !axesSet.y) return;
const cartesian = new (is3d ? Axes.Cartesian3dAxes : Axes.Cartesian2dAxes)(core);
cartesian.isDivisionPickingEnabled = [false, false, false];
cartesian.arePickDivisionsVisible = [false, false, false];
cartesian.isLabelPickingEnabled = [false, false, false];
cartesian.isTitlePickingEnabled = [false, false, false];
cartesian.isGridPickingEnabled = false;
cartesian.isHeadingPickingEnabled = [false, false, false];
createAxes(cartesian, 0, 0, axesSet.x, AxesTextOrientation.perpendicular, height, props, axesSet.showFacetTitleX && facetLabelX);
createAxes(cartesian, 1, 1, axesSet.y, AxesTextOrientation.perpendicular, height, props, axesSet.showFacetTitleY && facetLabelY);
if (is3d) {
createAxes(cartesian, 1, 2, axesSet.z, AxesTextOrientation.perpendicular, height, props);
}
configCartesianAxes(is3d, bounds, cartesian);
const {
maxBoundsX,
maxBoundsY,
minBoundsX,
minBoundsY,
} = bounds;
const w = maxBoundsX - minBoundsX;
const h = maxBoundsY - minBoundsY;
cartesian.scalingX = axesSetBounds.w / w;
cartesian.scalingY = axesSetBounds.h / h;
cartesian.offsetX = ((axesSetBounds.x - minBoundsX + axesSetBounds.w / 2) / w) - 0.5;
cartesian.offsetY = ((axesSetBounds.y - minBoundsY + axesSetBounds.h / 2) / h) - 0.5;
const aspect = (h / w);
if (aspect > 1) {
cartesian.offsetX /= aspect;
} else {
cartesian.offsetY *= aspect;
}
const axes = is3d ? renderer.createCartesian3dAxesVisual(<Cartesian3dAxes>cartesian) : renderer.createCartesian2dAxesVisual(<Cartesian2dAxes>cartesian);
renderer.currentAxes.push(axes);
props.config.onAxesComplete && props.config.onAxesComplete(cartesian);
});
return { bounds };
};
const nullDomain: StyledLine = {
sourcePosition: [0, 0, 0],
targetPosition: [0, 0, 0],
};
interface AxesSetBounds extends IBounds {
x?: number;
y?: number;
h?: number;
w?: number;
axesSet: AxesSet;
}
type AxesSet = {
rendered?: boolean;
x?: Axis;
y?: Axis;
z?: Axis;
showFacetTitleX?: boolean;
showFacetTitleY?: boolean;
}
class AxesCorrelation {
axesSets: AxesSet[];
labels: Axis[];
constructor(stage: Stage, private dimensions: number) {
const { x, y, z } = stage.axes;
this.axesSets = [];
this.labels = [];
[x, y, z].forEach(axes => {
axes.forEach(axis => {
if (this.axesSets.length === 0) {
this.initialize(axis);
} else {
this.correlate(axis);
}
});
});
}
getGrid() {
const mapCols: { [col: string]: { [row: string]: AxesSet } } = {};
const mapRows: { [row: string]: null } = {};
this.axesSets.forEach(axesSet => {
const domain = axesSet?.x?.domain;
if (!domain) return;
const col = domain.sourcePosition[0].toString();
const row = domain.sourcePosition[1].toString();
if (!mapCols[col]) {
mapCols[col] = {};
}
mapCols[col][row] = axesSet;
mapRows[row] = null;
});
const colKeys = Object.keys(mapCols).sort((a, b) => +a - +b);
const rowKeys = Object.keys(mapRows).sort((a, b) => +a - +b);
return {
cols: colKeys.length,
rows: rowKeys.length,
byColumn: colKeys.map(colKey => rowKeys.map(rowKey => { return { colKey, rowKey, axesSet: mapCols[colKey][rowKey] }; })),
byRow: rowKeys.map(rowKey => colKeys.map(colKey => { return { colKey, rowKey, axesSet: mapCols[colKey][rowKey] }; })),
};
}
initialize(axis: Axis) {
if (!axis.domain) {
this.labels.push(axis);
return;
}
const axesSet: AxesSet = {};
axesSet[axis.axisRole] = axis;
this.axesSets.push(axesSet);
}
correlate(axis: Axis) {
if (!axis.domain) {
this.labels.push(axis);
return;
}
for (let i = 0; i < this.axesSets.length; i++) {
const axesSet = this.axesSets[i];
for (const axisRole in axesSet) {
const test: Axis = axesSet[axisRole];
if (this.matchDomains(axis.domain, test.domain)) {
//prefer the axes with titles
if (!axesSet[axis.axisRole] || (!axesSet[axis.axisRole].tickText.length && axis.tickText.length)) {
axesSet[axis.axisRole] = axis;
}
return;
}
}
}
this.initialize(axis);
}
matchDomains(a: StyledLine, b: StyledLine) {
if (this.matchPoint(a.sourcePosition, b.sourcePosition)) return true;
if (this.matchPoint(a.sourcePosition, b.targetPosition)) return true;
if (this.matchPoint(a.targetPosition, b.targetPosition)) return true;
if (this.matchPoint(a.targetPosition, b.sourcePosition)) return true;
return false;
}
matchPoint(a: Vec3, b: Vec3) {
for (let i = 0; i < this.dimensions; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
}
function createAxes(cartesian: Axes.Cartesian2dAxes | Axes.Cartesian3dAxes, dim2d: number, dim3d: number, axis: Axis, orientation: AxesTextOrientation, height: number, props: ILayerProps, facetLabel?: Axis) {
const domain = axis?.domain || nullDomain;
const { tickPositions, tickText, textPos, textSize } = convertAxis(axis, domain, dim2d, height);
cartesian.setTickPositions(dim3d, tickPositions);
cartesian.zero[dim3d] = 0; //TODO get any "zero" gridline position from vega
cartesian.setLabelPositions(dim3d, textPos);
cartesian.setLabels(dim3d, tickText);
cartesian.setLabelSizes(dim3d, textSize);
const title = axis?.title || facetLabel?.title;
if (title?.text) {
cartesian.setTitle(dim3d, title.text);
cartesian.setTitleSize(dim3d, title.size / height);
}
cartesian.setLabelOrientation(dim3d, orientation);
props.config.onAxisConfig && props.config.onAxisConfig(cartesian, dim3d, axis);
return {
tickText,
};
}
function configCartesianAxes(is3d: boolean, bounds: IBounds, cartesian: Axes.Cartesian2dAxes | Axes.Cartesian3dAxes) {
if (is3d) {
cartesian.isEdgeVisible[Edge3D.topBack] = false;
}
cartesian.isEdgeVisible[Edge3D.backRight] = false;
cartesian.isEdgeVisible[Edge3D.bottomRight] = false;
cartesian.isEdgeVisible[Edge3D.frontRight] = false;
cartesian.isEdgeVisible[Edge3D.topFront] = false;
cartesian.isEdgeVisible[Edge3D.topRight] = false;
const {
maxBoundsX,
maxBoundsY,
maxBoundsZ,
minBoundsX,
minBoundsY,
minBoundsZ,
} = bounds;
cartesian.minBoundsX = minBoundsX;
cartesian.maxBoundsX = maxBoundsX;
cartesian.minBoundsY = minBoundsY;
cartesian.maxBoundsY = maxBoundsY;
if (is3d) {
(<Axes.Cartesian3dAxes>cartesian).minBoundsZ = minBoundsZ;
(<Axes.Cartesian3dAxes>cartesian).maxBoundsZ = maxBoundsZ;
}
}
function getDomainBounds(dim2d: number, axis: Axis) {
const domain = axis?.domain || nullDomain;
const minBounds = domain.sourcePosition[dim2d];
const maxBounds = domain.targetPosition[dim2d];
return {
maxBounds,
minBounds,
};
}
function convertAxis(axis: Axis, domain: StyledLine, dim: number, height: number) {
const tickPositions = axis
?
axis.ticks.map(t => (t.sourcePosition[dim] - domain.sourcePosition[dim]) / (domain.targetPosition[dim] - domain.sourcePosition[dim]))
:
[];
const tickText = axis ?
axis.tickText.map(t => t.text)
:
[];
const textPos = axis ?
axis.tickText.map(t => (t.position[dim] - domain.sourcePosition[dim]) / (domain.targetPosition[dim] - domain.sourcePosition[dim]))
:
[];
const textSize = axis ?
axis.tickText.map(t => t.size / height)
:
[];
if (tickPositions.length) {
if (tickPositions[0] !== 0) {
tickPositions[0] = 0;
}
if (tickPositions[tickPositions.length - 1] !== 1) {
tickPositions[tickPositions.length - 1] = 1;
}
}
return {
tickPositions,
tickText,
textPos,
textSize,
};
}

Просмотреть файл

@ -0,0 +1,52 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { IBounds } from '../interfaces';
export function outerBounds(b1: IBounds, b2: IBounds): IBounds {
if (!b1 && !b2) return;
if (!b1) return b2;
if (!b2) return b1;
const minProps = [
'minBoundsX',
'minBoundsY',
'minBoundsZ',
];
const maxProps = [
'maxBoundsX',
'maxBoundsY',
'maxBoundsZ',
];
const result = {} as IBounds;
minProps.forEach(p => result[p] = notNull(Math.min, b1[p], b2[p]));
maxProps.forEach(p => result[p] = notNull(Math.max, b1[p], b2[p]));
return result;
}
function notNull(fn: (...values: number[]) => number, v1: number, v2: number) {
if (v1 == null && v2 == null) return null;
if (v1 == null) return v2;
if (v2 == null) return v1;
return fn(v1, v2);
}
export function increment(
b: IBounds,
minBoundsX: number,
minBoundsY: number,
minBoundsZ: number,
maxBoundsX: number,
maxBoundsY: number,
maxBoundsZ: number,
) {
return outerBounds(b, {
minBoundsX,
minBoundsY,
minBoundsZ,
maxBoundsX,
maxBoundsY,
maxBoundsZ,
});
}

Просмотреть файл

@ -0,0 +1,41 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { RGBAColor } from '../interfaces';
export class ColorMap {
private colorMap: { [key: string]: { index: number, rgbaColor: RGBAColor } };
private colorArray: RGBAColor[];
constructor(public quant = 5) {
this.colorMap = {};
this.colorArray = [];
}
private getColorKey(rgbaColor: RGBAColor) {
const color = rgbaColor.slice(0, 3).map(e => Math.floor(e / this.quant) * this.quant);
color[3] = rgbaColor[3]; //retain alpha
return JSON.stringify(color);
}
public registerColor(rgbaColor: RGBAColor) {
const colorKey = this.getColorKey(rgbaColor);
if (!this.colorMap[colorKey]) {
this.colorMap[colorKey] = {
index: this.colorArray.length,
rgbaColor,
};
this.colorArray.push(rgbaColor);
}
return this.colorMap[colorKey].index;
}
public getPalette() {
return {
palette: new Uint8Array(this.colorArray.flat()),
maxColor: this.colorArray.length - 1,
};
}
}

Просмотреть файл

@ -0,0 +1,146 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { Layouts, UnitType } from 'morphcharts';
import { IBounds, ILayer, ILayerCreator, ILayerProps, Stage } from '../interfaces';
import { increment } from './bounds';
import { ColorMap } from './color';
const key = 'cube';
export const createCubeLayer: ILayerCreator = (props: ILayerProps): ILayer => {
const { ref, stage } = props;
const { core } = ref;
const scatter = new Layouts.Scatter(core);
const {
ids,
colors,
positionsX,
positionsY,
positionsZ,
sizesX,
sizesY,
sizesZ,
bounds,
maxColor,
palette,
} = convert(stage);
if (!ids.length) return;
const { renderer } = core;
let cubeTransitionBuffer = renderer.transitionBuffers.find(t => t.key === key);
if (!cubeTransitionBuffer) {
cubeTransitionBuffer = renderer.createTransitionBuffer(ids);
cubeTransitionBuffer.key = key;
renderer.transitionBuffers.push(cubeTransitionBuffer);
} else {
cubeTransitionBuffer.swap();
}
scatter.layout(cubeTransitionBuffer.currentBuffer, ids, {
positionsX,
positionsY,
positionsZ,
});
const layer: ILayer = {
update: (newBounds, selected) => {
const { colors, maxColor, minColor, palette } = layer.unitColorMap;
// reference off of core.renderer to get the actual buffer
const currCubeTransitionBuffer = core.renderer.transitionBuffers.find(t => t.key === key);
currCubeTransitionBuffer.currentBuffer.unitType = UnitType.block;
currCubeTransitionBuffer.currentPalette.colors = palette;
scatter.update(currCubeTransitionBuffer.currentBuffer, ids, {
selected,
colors,
minColor,
maxColor,
sizesX,
sizesY,
sizesZ,
...newBounds,
});
},
bounds,
unitColorMap: {
colors,
ids,
minColor: 0,
maxColor,
palette,
},
};
return layer;
};
function convert(stage: Stage) {
const { cubeData } = stage;
const { length } = cubeData;
const ids: number[] = [];
const colors = new Float64Array(length);
const positionsX = new Float64Array(length);
const positionsY = new Float64Array(length);
const positionsZ = new Float64Array(length);
const sizesX = new Float64Array(length);
const sizesY = new Float64Array(length);
const sizesZ = new Float64Array(length);
let bounds: IBounds;
const colorMap = new ColorMap();
cubeData.forEach((cube, i) => {
ids.push(i);
if (cube.isEmpty) {
positionsX[i] = 0;
positionsY[i] = 0;
positionsZ[i] = 0;
sizesX[i] = 0;
sizesY[i] = 0;
sizesZ[i] = 0;
colors[i] = 0;
} else {
//ids.push(cube.ordinal);
positionsX[i] = cube.position[0] + cube.size[0] * 0.5;
positionsY[i] = cube.position[1] + cube.size[1] * 0.5;
positionsZ[i] = cube.position[2] + cube.size[2] * 0.5;
sizesX[i] = cube.size[0];
sizesY[i] = cube.size[1];
sizesZ[i] = cube.size[2];
bounds = increment(bounds,
cube.position[0],
cube.position[1],
cube.position[2],
cube.position[0] + cube.size[0],
cube.position[1] + cube.size[1],
cube.position[2] + cube.size[2],
);
colors[i] = colorMap.registerColor(cube.color);
}
});
const { palette, maxColor } = colorMap.getPalette();
return {
ids: new Uint32Array(ids),
colors,
positionsX,
positionsY,
positionsZ,
sizesX,
sizesY,
sizesZ,
bounds,
maxColor,
palette,
};
}

Просмотреть файл

@ -0,0 +1,41 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { Components, Core } from 'morphcharts';
import { vec3 } from 'gl-matrix';
import { IBounds } from '../interfaces';
export function getImageData(url: string) {
return new Promise<ImageData>((resolve, reject) => {
const imageElement = document.createElement('img');
imageElement.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const { height, width } = imageElement;
canvas.width = width;
canvas.height = height;
ctx.drawImage(imageElement, 0, 0);
resolve(ctx.getImageData(0, 0, width, height));
};
imageElement.src = url;
});
}
export function createImageQuad(core: Core, imageData: ImageData, bounds: IBounds, position: vec3, width: number, height: number) {
const { maxBoundsX, maxBoundsY, maxBoundsZ, minBoundsX, minBoundsY, minBoundsZ } = bounds;
const imageOptions: Components.IImageQuadOptions = {
imageData,
position,
height,
width,
minBoundsX,
maxBoundsX,
minBoundsZ,
maxBoundsZ,
minBoundsY,
maxBoundsY,
};
return new Components.ImageQuad(core, imageOptions);
}

Просмотреть файл

@ -0,0 +1,385 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { Constants, Core, Helpers, Input, SingleTouchAction } from 'morphcharts';
import { colorFromString } from '../color';
import { MorphChartsColorMapper, UnitColorMap } from '../exports/types';
import { IBounds, ILayerProps, MorphChartsRendering, MorphChartsRef, PreStage, Stage, McColor, McColors, PresenterConfig, McRendererOptions, LayerSelection, ILayer, ImageBounds } from '../interfaces';
import { createAxesLayer } from './axes';
import { outerBounds } from './bounds';
import { createCubeLayer } from './cubes';
import { createLineLayer } from './lines';
import { getRenderer, rendererEnabled, setRendererOptions, shouldChangeRenderer } from './renderer';
import { createTextLayer } from './text';
import { quat } from 'gl-matrix';
import { easing } from '../easing';
import { createImageQuad, getImageData } from './image';
import { minZ } from '../defaults';
import { vec3 } from 'gl-matrix';
export { MorphChartsRef };
export interface McOptions {
container: HTMLElement;
pickGridCallback: (divisions: number[], e: MouseEvent | PointerEvent | TouchEvent) => void;
onCubeHover: (e: MouseEvent, id: number) => void;
onCubeClick: (e: MouseEvent, id: number) => void;
onCanvasClick: (e: MouseEvent) => void;
onLasso: (ids: Set<number>, e: MouseEvent | PointerEvent | TouchEvent) => void;
}
export function init(options: McOptions, mcRendererOptions: McRendererOptions) {
const { container, pickGridCallback } = options;
const core = new Core({ container });
getRenderer(mcRendererOptions, core);
core.config.pickSelectDelay = 50;
const { inputManager } = core;
inputManager.pickAxesGridCallback = ({ divisionX, divisionY, divisionZ, manipulator }) => {
clearClickTimeout();
const { altKey, button, shiftKey } = manipulator;
const me = { altKey, shiftKey, button } as MouseEvent;
const e: MouseEvent | PointerEvent | TouchEvent = me;
pickGridCallback([divisionX, divisionY, divisionZ], e);
};
inputManager.pickLassoCallback = result => {
options.onLasso(result.ids[0], result.manipulator.event as MouseEvent);
};
const rightButton = 2;
inputManager.singleTouchAction = manipulator => {
if (manipulator.button == rightButton || manipulator.shiftKey || manipulator.ctrlKey) {
return SingleTouchAction.rotate;
}
else if (manipulator.altKey) {
return SingleTouchAction.lasso;
}
else {
return SingleTouchAction.translate;
}
};
//synthesize a hover event
const canvas = container.getElementsByTagName('canvas')[0];
let pickedId: number;
const hover = (e: MouseEvent) => {
if (core.renderer.pickedId !== pickedId) {
pickedId = core.renderer.pickedId;
const ordinal = core.renderer.transitionBuffers[0].pickIdLookup[pickedId];
options.onCubeHover(e, ordinal);
}
};
canvas.addEventListener('mousemove', (e) => {
clearClickTimeout();
if (mousedown) {
options.onCubeHover(e, null);
}
hover(e);
});
canvas.addEventListener('mouseout', hover);
canvas.addEventListener('mouseover', hover);
let mousedown: boolean;
canvas.addEventListener('mousedown', (e) => {
mousedown = true;
});
canvas.addEventListener('mouseup', (e) => {
mousedown = false;
});
let canvasClickTimeout: NodeJS.Timeout;
const clearClickTimeout = () => {
clearTimeout(canvasClickTimeout);
canvasClickTimeout = null;
};
canvas.addEventListener('click', (e) => {
canvasClickTimeout = setTimeout(() => {
options.onCanvasClick(e);
}, 50);
});
inputManager.pickItemCallback = ({ manipulator }) => {
clearClickTimeout();
const ordinal = core.renderer.transitionBuffers[0].pickIdLookup[pickedId];
options.onCubeClick(manipulator.event as MouseEvent, ordinal);
};
const ref: MorphChartsRef = {
supportedRenders: {
advanced: rendererEnabled(true),
basic: rendererEnabled(false),
},
reset: () => {
core.reset(true);
quat.slerp(ref.qModelCurrent, ref.qModelTo, ref.qModelTo, 0);
core.setModelRotation(ref.qModelCurrent, true);
core.camera.setOrbit(ref.qCameraRotationTo, false);
//core.camera.setPosition(ref.vCameraPositionTo, false);
},
transitionModel: false,
qModelFrom: null,
qModelTo: null,
qModelCurrent: quat.create(),
qCameraRotationFrom: quat.create(),
qCameraRotationTo: null,
qCameraRotationCurrent: quat.create(),
vCameraPositionFrom: vec3.create(),
vCameraPositionTo: null,
vCameraPositionCurrent: vec3.create(),
core,
cameraTime: 0,
isCameraMovement: false,
isTransitioning: false,
transitionTime: 0,
setMcRendererOptions(mcRendererOptions: McRendererOptions) {
if (shouldChangeRenderer(ref.lastMcRendererOptions, mcRendererOptions)) {
getRenderer(mcRendererOptions, core);
} else {
if (mcRendererOptions.advanced) {
//same renderer, poke the config
setRendererOptions(core.renderer, mcRendererOptions);
}
}
ref.lastMcRendererOptions = mcRendererOptions;
},
lastMcRendererOptions: mcRendererOptions,
};
const cam = (t: number) => {
quat.slerp(ref.qCameraRotationCurrent, ref.qCameraRotationFrom, ref.qCameraRotationTo, t);
vec3.lerp(ref.vCameraPositionCurrent, ref.vCameraPositionFrom, ref.vCameraPositionTo, t);
core.camera.setOrbit(ref.qCameraRotationCurrent, false);
core.camera.setPosition(ref.vCameraPositionCurrent, false);
// disable picking during transitions, as the performance degradation could reduce the framerate
inputManager.isPickingEnabled = false;
};
core.updateCallback = (elapsedTime) => {
if (ref.isTransitioning) {
ref.transitionTime += elapsedTime;
const totalTime = core.config.transitionDuration + core.config.transitionStaggering;
if (ref.transitionTime >= totalTime) {
ref.isTransitioning = false;
ref.transitionTime = totalTime;
}
const t = easing(ref.transitionTime / totalTime);
core.renderer.transitionTime = t;
if (ref.transitionModel) {
quat.slerp(ref.qModelCurrent, ref.qModelFrom, ref.qModelTo, t);
core.setModelRotation(ref.qModelCurrent, false);
}
cam(t);
} else if (ref.isCameraMovement) {
ref.cameraTime += elapsedTime;
const totalTime = core.config.transitionDuration;
if (ref.cameraTime >= totalTime) {
ref.isCameraMovement = false;
ref.cameraTime = totalTime;
}
const t = easing(ref.cameraTime / totalTime);
cam(t);
} else {
inputManager.isPickingEnabled = true;
}
};
return ref;
}
const qModel2d = quat.create();
const qModel3d = Constants.QUAT_ROTATEX_MINUS_90;
const qCameraRotation2d = quat.create();
const qCameraRotation3d = quat.create();
const qAngle = quat.create();
const vPosition = vec3.create();
// Altitude (pitch around local right axis)
quat.setAxisAngle(qCameraRotation3d, Constants.VECTOR3_UNITX, Helpers.AngleHelper.degreesToRadians(30));
// Azimuth (yaw around global up axis)
quat.setAxisAngle(qAngle, Constants.VECTOR3_UNITY, Helpers.AngleHelper.degreesToRadians(-25));
quat.multiply(qCameraRotation3d, qCameraRotation3d, qAngle);
export function mcRender(ref: MorphChartsRef, prevStage: Stage, stage: Stage, height: number, width: number, preStage: PreStage, colors: McColors, config: PresenterConfig): MorphChartsRendering {
const cameraTo = config.getCameraTo && config.getCameraTo();
if (prevStage && (prevStage.view !== stage.view)) {
ref.transitionModel = true;
if (stage.view === '2d') {
ref.qModelFrom = qModel3d;
ref.qModelTo = qModel2d;
ref.qCameraRotationTo = cameraTo?.rotation || qCameraRotation2d;
ref.vCameraPositionTo = cameraTo?.position || vPosition;
} else {
ref.qModelFrom = qModel2d;
ref.qModelTo = qModel3d;
ref.qCameraRotationTo = cameraTo?.rotation || qCameraRotation3d;
ref.vCameraPositionTo = cameraTo?.position || vPosition;
}
} else {
if (stage.view === '2d') {
ref.qModelTo = qModel2d;
ref.qCameraRotationTo = cameraTo?.rotation || qCameraRotation2d;
ref.vCameraPositionTo = cameraTo?.position || vPosition;
} else {
ref.qModelTo = qModel3d;
ref.qCameraRotationTo = cameraTo?.rotation || qCameraRotation3d;
ref.vCameraPositionTo = cameraTo?.position || vPosition;
}
ref.transitionModel = false;
}
ref.core.camera.getOrbit(ref.qCameraRotationFrom);
ref.core.camera.getPosition(ref.vCameraPositionFrom);
if (!prevStage) {
ref.core.setModelRotation(ref.qModelTo, false);
ref.core.camera.setOrbit(ref.qCameraRotationTo, false);
ref.core.camera.setPosition(ref.vCameraPositionTo, false);
}
const props: ILayerProps = { ref, stage, height, width, config };
const cubeLayer = createCubeLayer(props);
const lineLayer = createLineLayer(props);
const textLayer = createTextLayer(props);
const { backgroundImages } = stage;
let contentBounds = outerBounds(
outerBounds(cubeLayer?.bounds, lineLayer?.bounds),
outerBounds(textLayer?.bounds, null),
);
backgroundImages?.forEach(backgroundImage => {
contentBounds = outerBounds(contentBounds, convertBounds(backgroundImage.bounds));
});
props.bounds = contentBounds;
const axesLayer = createAxesLayer(props);
const { core } = ref;
core.config.transitionStaggering = config.transitionDurations.stagger;
core.config.transitionDuration = config.transitionDurations.position;
let bounds: IBounds;
if (axesLayer && axesLayer.bounds) {
bounds = axesLayer.bounds;
} else {
bounds = contentBounds;
}
const colorMapper: MorphChartsColorMapper = {
getCubeUnitColorMap: () => cubeLayer.unitColorMap,
setCubeUnitColorMap: (unitColorMap: UnitColorMap) => {
cubeLayer.unitColorMap = unitColorMap;
},
};
if (preStage) {
preStage(stage, colorMapper);
}
//add images
core.renderer.images = [];
if (backgroundImages) {
const addImage = (imageBounds: IBounds, imageData: ImageData) => {
const imageWidth = imageBounds.maxBoundsX - imageBounds.minBoundsX;
const imageHeight = imageBounds.maxBoundsY - imageBounds.minBoundsY;
const position: vec3 = [imageBounds.minBoundsX + imageWidth / 2, imageBounds.minBoundsY + imageHeight / 2, 0];
const imageQuad = createImageQuad(core, imageData, contentBounds, position, imageWidth, imageHeight);
const imageVisual = core.renderer.createImageVisual(imageQuad);
core.renderer.images.push(imageVisual);
};
const imageDataCache: { [key: string]: ImageData } = {};
backgroundImages.forEach(backgroundImage => {
const imageBounds = convertBounds(backgroundImage.bounds);
const imageData = imageDataCache[backgroundImage.url];
if (imageData) {
addImage(imageBounds, imageData);
} else {
getImageData(backgroundImage.url).then(imageData => {
imageDataCache[backgroundImage.url] = imageData;
addImage(imageBounds, imageData);
});
}
});
}
//Now call update on each layout
layersWithSelection(cubeLayer, lineLayer, textLayer, config.layerSelection, bounds);
ref.isTransitioning = true;
ref.transitionTime = 0;
core.renderer.transitionTime = 0; // Set renderer transition time for this render pass to prevent rendering target buffer for single frame
colorConfig(ref, colors);
return {
...colorMapper,
update: layerSelection => layersWithSelection(cubeLayer, lineLayer, textLayer, layerSelection, bounds),
activate: id => core.renderer.transitionBuffers[0].activeId = id,
moveCamera: (position: vec3, rotation: quat) => {
if (!ref.isTransitioning) {
ref.core.camera.getOrbit(ref.qCameraRotationFrom);
ref.core.camera.getPosition(ref.vCameraPositionFrom);
ref.isCameraMovement = true;
ref.cameraTime = 0;
ref.qCameraRotationTo = rotation;
ref.vCameraPositionTo = position;
}
},
};
}
function layersWithSelection(cubeLayer: ILayer, lineLayer: ILayer, textLayer: ILayer, layerSelection: LayerSelection, bounds: IBounds) {
const layers = [
{
layer: cubeLayer,
selection: layerSelection?.cubes,
},
{
layer: lineLayer,
selection: layerSelection?.lines,
},
{
layer: textLayer,
selection: layerSelection?.texts,
},
];
layers.forEach(x => x.layer?.update(bounds, x.selection));
}
function convert(newColor: string): McColor {
const c = colorFromString(newColor).slice(0, 3);
return c.map(v => v / 255) as McColor;
}
export function colorConfig(ref: MorphChartsRef, colors: McColors) {
if (!colors) return;
const { config } = ref.core;
config.activeColor = convert(colors.activeItemColor);
config.backgroundColor = convert(colors.backgroundColor);
config.textColor = convert(colors.textColor);
config.textBorderColor = convert(colors.textBorderColor);
config.axesTextColor = convert(colors.axesTextLabelColor);
config.axesGridBackgroundColor = convert(colors.axesGridBackgroundColor);
config.axesGridHighlightColor = convert(colors.axesGridHighlightColor);
config.axesGridMinorColor = convert(colors.axesGridMinorColor);
config.axesGridMajorColor = convert(colors.axesGridMajorColor);
config.axesGridZeroColor = convert(colors.axesGridZeroColor);
//TODO fix this - hack to reset the background color
ref.core.renderer['_theme'] = null;
}
function convertBounds(bounds: ImageBounds): IBounds {
if (!bounds) return;
return {
minBoundsX: bounds.x1,
maxBoundsX: bounds.x2,
minBoundsY: -bounds.y2,
maxBoundsY: -bounds.y1,
minBoundsZ: minZ,
maxBoundsZ: minZ,
};
}

Просмотреть файл

@ -0,0 +1,181 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { Layouts, UnitType } from 'morphcharts';
import { ILineVertexOptions } from 'morphcharts/dist/layouts/line';
import { Position, Stage } from '../interfaces';
import { increment } from './bounds';
import { ColorMap } from './color';
import { IBounds, ILayer, ILayerCreator, ILayerProps } from '../interfaces';
const key = 'line';
export const createLineLayer: ILayerCreator = (props: ILayerProps): ILayer => {
const { height, ref, stage, width } = props;
const { core } = ref;
const lines = new Layouts.Line(core);
const {
ids,
fromIds,
toIds,
lineColors,
lineSizes,
bounds,
positionsX,
positionsY,
positionsZ,
lineMaxColor,
palette,
} = convert(stage, height, width);
if (!ids.length) return;
const { renderer } = core;
let lineTransitionBuffer = renderer.transitionBuffers.find(t => t.key === key);
if (!lineTransitionBuffer) {
lineTransitionBuffer = renderer.createTransitionBuffer(ids);
lineTransitionBuffer.key = key;
renderer.transitionBuffers.push(lineTransitionBuffer);
} else {
lineTransitionBuffer.swap();
}
lines.layout(
lineTransitionBuffer.currentBuffer,
ids,
fromIds,
toIds,
{
positionsX,
positionsY,
positionsZ,
lineSizes,
sizeScaling: 1,
},
);
let options: ILineVertexOptions = {
lineColors,
lineMinColor: 0,
lineMaxColor,
};
// Unit type
lineTransitionBuffer.currentBuffer.unitType = UnitType.cylinder;
lineTransitionBuffer.currentPalette.colors = palette;
return {
update: newBounds => {
options = {
...options,
...newBounds,
};
// reference off of core.renderer to get the actual buffer
const currLineTransitionBuffer = core.renderer.transitionBuffers.find(t => t.key === key);
lines.update(
currLineTransitionBuffer.currentBuffer,
ids,
fromIds,
toIds,
options,
);
},
bounds,
unitColorMap: {
ids,
colors: lineColors,
minColor: 0,
maxColor: lineMaxColor,
palette,
},
};
};
interface Line {
id: number;
from: number;
to: number;
color: number;
size: number;
}
function convert(stage: Stage, height: number, width: number) {
const { pathData } = stage;
const positions: Position[] = [];
const lines: Line[] = [];
const colorMap = new ColorMap();
pathData.forEach(path => {
const color = colorMap.registerColor(path.strokeColor);
let from = positions.length;
positions.push(path.positions[0]);
for (let i = 1; i < path.positions.length; i++) {
const to = positions.length;
positions.push(path.positions[i]);
lines.push({
id: lines.length,
from,
to,
color,
size: path.strokeWidth,
});
from = to;
}
});
const ids = new Uint32Array(lines.length);
const fromIds = new Uint32Array(lines.length);
const toIds = new Uint32Array(lines.length);
const lineColors = new Float64Array(lines.length);
const lineSizes = new Float64Array(lines.length);
lines.forEach((line, i) => {
ids[i] = i;
fromIds[i] = line.from;
toIds[i] = line.to;
lineColors[i] = line.color;
lineSizes[i] = line.size;
});
const positionsX = new Float64Array(positions.length);
const positionsY = new Float64Array(positions.length);
const positionsZ = new Float64Array(positions.length);
let bounds: IBounds;
positions.forEach((p, i) => {
positionsX[i] = p[0];
positionsY[i] = p[1] + height;
positionsZ[i] = p[2];
bounds = increment(bounds,
positionsX[i],
positionsY[i],
positionsZ[i],
positionsX[i],
positionsY[i],
positionsZ[i],
);
});
const { palette, maxColor: lineMaxColor } = colorMap.getPalette();
return {
ids,
fromIds,
toIds,
lineColors,
lineSizes,
bounds,
positionsX,
positionsY,
positionsZ,
lineMaxColor,
palette,
};
}

Просмотреть файл

@ -0,0 +1,47 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { Core, Renderers } from 'morphcharts';
import { RendererBase } from 'morphcharts/dist/renderers/renderer';
import { McRendererOptions } from '../interfaces';
export function shouldChangeRenderer(prev: McRendererOptions, next: McRendererOptions) {
if (!prev || !next) return true;
if (prev.advanced !== next.advanced) return true;
if (!prev.advanced) {
return prev.basicOptions?.antialias != next.basicOptions?.antialias;
}
}
export function getRenderer(mcRendererOptions: McRendererOptions, core: Core) {
const advanced = mcRendererOptions?.advanced;
const r = advanced ?
new Renderers.Advanced.Main()
:
new Renderers.Basic.Main(mcRendererOptions?.basicOptions)
;
core.renderer = r;
setRendererOptions(r, mcRendererOptions);
return r;
}
export function setRendererOptions(renderer: RendererBase, mcRendererOptions: McRendererOptions) {
const o = mcRendererOptions?.advancedOptions;
if (mcRendererOptions?.advanced && o) {
for (const key in o) {
renderer.config[key] = o[key];
}
}
}
export function rendererEnabled(advanced: boolean) {
const r = advanced ?
new Renderers.Advanced.Main()
:
new Renderers.Basic.Main()
;
return r.isSupported;
}

Просмотреть файл

@ -0,0 +1,124 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { Components, HorizontalAlignment, VerticalAlignment } from 'morphcharts';
import { IBounds, ILayer, ILayerCreator, ILayerProps, Stage } from '../interfaces';
import { increment } from './bounds';
import { ColorMap } from './color';
export const createTextLayer: ILayerCreator = (props: ILayerProps): ILayer => {
const { ref, stage } = props;
const { core } = ref;
const {
ids,
colors,
positionsX,
positionsY,
positionsZ,
sizes,
bounds,
maxColor,
maxGlyphs,
palette,
text,
} = convert(stage);
if (text.length === 0) {
core.renderer.labelSets = [];
return;
}
const options: Components.ILabelSetOptions = {
text,
maxGlyphs,
scales: sizes,
};
const labelSet = new Components.LabelSet(core, options);
labelSet.positionsX = positionsX;
labelSet.positionsY = positionsY;
labelSet.positionsZ = positionsZ;
labelSet.horizontalAlignment = HorizontalAlignment.center;
labelSet.verticalAlignment = VerticalAlignment.center;
const layer: ILayer = {
update: bounds => {
const {
maxBoundsX,
maxBoundsY,
maxBoundsZ,
minBoundsX,
minBoundsY,
minBoundsZ,
} = bounds;
labelSet.minBoundsX = minBoundsX;
labelSet.minBoundsY = minBoundsY;
labelSet.minBoundsZ = minBoundsZ;
labelSet.maxBoundsX = maxBoundsX;
labelSet.maxBoundsY = maxBoundsY;
labelSet.maxBoundsZ = maxBoundsZ;
},
bounds,
};
const labelSetVisual = core.renderer.createLabelSetVisual(labelSet);
core.renderer.labelSets = [labelSetVisual];
return layer;
};
function convert(stage: Stage) {
const { textData } = stage;
const { length } = textData;
const ids: number[] = [];
const text: string[] = [];
const colors = new Float64Array(length);
const positionsX = new Float64Array(length);
const positionsY = new Float64Array(length);
const positionsZ = new Float64Array(length);
const sizes = new Float64Array(length);
let bounds: IBounds;
let maxGlyphs = 0;
const colorMap = new ColorMap();
textData.forEach((t, i) => {
ids.push(i);
text.push(t.text);
maxGlyphs += t.text.length;
positionsX[i] = t.position[0];
positionsY[i] = t.position[1];
positionsZ[i] = t.position[2];
sizes[i] = t.size;
bounds = increment(bounds,
t.position[0],
t.position[1],
t.position[2],
t.position[0],
t.position[1],
t.position[2],
);
colors[i] = colorMap.registerColor(t.color);
});
const { palette, maxColor } = colorMap.getPalette();
return {
ids: new Uint32Array(ids),
colors,
positionsX,
positionsY,
positionsZ,
sizes,
bounds,
maxColor,
maxGlyphs,
palette,
text,
};
}

Просмотреть файл

@ -0,0 +1,31 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { createElement, mount } from 'tsx-create-element';
import { minHeight, minWidth } from './defaults';
import { PresenterElement } from './enums';
import { PresenterStyle } from './interfaces';
export interface IPresenter {
el: HTMLElement;
style: PresenterStyle;
}
export function initializePanel(presenter: IPresenter) {
const rootDiv = (
<div className={className(PresenterElement.root, presenter)}>
<div className={className(PresenterElement.gl, presenter)} style={{ minHeight, minWidth }}></div>
<div className={className(PresenterElement.panel, presenter)}>
<div className={className(PresenterElement.vegaControls, presenter)}></div>
<div className={className(PresenterElement.legend, presenter)}></div>
</div>
</div>
);
mount(rootDiv, presenter.el);
}
export function className(type: PresenterElement, presenter: IPresenter) {
return `${presenter.style.cssPrefix}${PresenterElement[type]}`;
}

Просмотреть файл

@ -0,0 +1,15 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { Cube } from './interfaces';
export function patchCubeArray(allocatedSize: number, empty: Partial<Cube>, cubes: Cube[]) {
const patched: Cube[] = new Array<Cube>(allocatedSize);
patched.fill(empty as Cube);
cubes.forEach(cube => patched[cube.ordinal] = cube);
return patched;
}

Просмотреть файл

@ -0,0 +1,257 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { deepMerge } from './clone';
import { createStage, defaultOnAxisItem, defaultPresenterConfig, defaultPresenterStyle } from './defaults';
import { PresenterElement } from './enums';
import {
Cube,
PresenterConfig,
PresenterStyle,
QueuedAnimationOptions,
Scene3d,
Stage,
MorphChartsRendering,
McColors,
} from './interfaces';
import { LegendView } from './legend';
import { MarkStagerOptions } from './marks/interfaces';
import { className, initializePanel } from './panel';
import { patchCubeArray } from './patchedCubeArray';
import { sceneToStage } from './stagers';
import { View } from '@msrvida/chart-types';
import { getActiveElementInfo, mount, setActiveElement } from 'tsx-create-element';
import { colorConfig, init, McOptions, mcRender, MorphChartsRef } from './morphcharts';
interface IBounds {
view: View;
height: number;
width: number;
cubeCount: number;
}
/**
* Class which presents a Stage of chart data using MorphCharts to render.
*/
export class Presenter {
/**
* Handle of the timer for animation.
*/
public animationTimer: number;
/**
* MorphCharts instance for rendering WebGL.
*/
public morphchartsref: MorphChartsRef;
/**
* Handle to recent MorphCharts rendering result.
*/
public mcRenderResult: MorphChartsRendering
/**
* Logger, such as console.log
*/
public logger: (message?: any, ...optionalParams: any[]) => void;
/**
* Get the previously rendered Stage object.
*/
get stage(): Stage {
return this._last.stage;
}
/**
* Options for styling the output.
*/
public style: PresenterStyle;
/**
* Get the current View camera type.
*/
get view(): View {
return this._last.view;
}
private queuedAnimationOptions: QueuedAnimationOptions;
private _mcOptions: McOptions;
private _last: IBounds & { stage: Stage };
/**
* Instantiate a new Presenter.
* @param el Parent HTMLElement to present within.
* @param style Optional PresenterStyle styling options.
*/
constructor(public el: HTMLElement, style?: PresenterStyle) {
this.style = deepMerge<PresenterStyle>(defaultPresenterStyle, style);
initializePanel(this);
this._last = { view: null, height: null, width: null, cubeCount: null, stage: null };
}
/**
* Cancels any pending animation, calling animationCanceled() on original queue.
*/
animationCancel() {
if (this.animationTimer) {
clearTimeout(this.animationTimer);
this.animationTimer = null;
if (this.logger) {
this.logger(`canceling animation ${(this.queuedAnimationOptions && this.queuedAnimationOptions.handlerLabel) || 'handler'}`);
}
if (this.queuedAnimationOptions && this.queuedAnimationOptions.animationCanceled) {
this.queuedAnimationOptions.animationCanceled.call(null);
}
}
}
/**
* Stops the current animation and queues a new animation.
* @param handler Function to invoke when timeout is complete.
* @param timeout Length of time to wait before invoking the handler.
* @param options Optional QueuedAnimationOptions object.
*/
animationQueue(handler: () => void, timeout: number, options?: QueuedAnimationOptions) {
if (this.logger) {
this.logger(`queueing animation ${(options && options.waitingLabel) || 'waiting'}...(${timeout})`);
}
this.animationCancel();
this.animationTimer = setTimeout(() => {
if (this.logger) {
this.logger(`queueing animation ${(options && options.handlerLabel) || 'handler'}...`);
}
handler();
}, timeout) as any as number;
}
/**
* Retrieve a sub-element of the rendered output.
* @param type PresenterElement type of the HTMLElement to retrieve.
*/
getElement(type: PresenterElement): HTMLElement {
const elements = this.el.getElementsByClassName(className(type, this));
if (elements && elements.length) {
return elements[0] as HTMLElement;
}
}
/**
* Present the Vega Scene, or Stage object using Morphcharts.
* @param sceneOrStage Vega Scene object, or Stage object containing chart layout info.
* @param height Height of the rendering area.
* @param width Width of the rendering area.
* @param config Optional presentation configuration object.
*/
present(sceneOrStage: Scene3d | Stage, height: number, width: number, config?: PresenterConfig) {
this.animationCancel();
const scene = sceneOrStage as Scene3d;
let stage: Stage;
const options: MarkStagerOptions = {
maxOrdinal: 0,
currAxis: null,
defaultCubeColor: this.style.defaultCubeColor,
assignCubeOrdinal: (config && config.onSceneRectAssignCubeOrdinal) || (() => options.maxOrdinal++),
modifyAxis: config?.onAxisItem ? config.onAxisItem : defaultOnAxisItem,
zAxisZindex: config?.zAxisZindex,
};
//determine if this is a vega scene
if (scene.marktype) {
stage = createStage(scene.view);
sceneToStage(options, stage, scene);
} else {
stage = sceneOrStage as Stage;
}
const c = deepMerge(defaultPresenterConfig, config);
if (!this.morphchartsref) {
this._mcOptions = {
container: this.getElement(PresenterElement.gl),
pickGridCallback: c.axisPickGridCallback,
onCubeHover: (e, ordinal) => {
c.onCubeHover(e, { ordinal, color: null, position: null, size: null });
},
onCubeClick: (e, ordinal) => {
c.onCubeClick(e, { ordinal, color: null, position: null, size: null });
},
onCanvasClick: config?.onLayerClick,
onLasso: config?.onLasso,
};
this.morphchartsref = init(this._mcOptions, c.initialMcRendererOptions || defaultPresenterConfig.initialMcRendererOptions);
}
let cubeCount = Math.max(this._last.cubeCount, stage.cubeData.length);
if (options.maxOrdinal) {
cubeCount = Math.max(cubeCount, options.maxOrdinal);
const empty: Partial<Cube> = {
isEmpty: true,
};
stage.cubeData = patchCubeArray(cubeCount, empty, stage.cubeData as Cube[]);
}
config.preLayer && config.preLayer(stage);
this.mcRenderResult = mcRender(this.morphchartsref, this._last.stage, stage, height, width, config && config.preStage, config && config.mcColors, c);
delete stage.cubeData;
delete stage.redraw;
this._last = {
cubeCount,
height,
width,
stage,
view: stage.view,
};
const a = getActiveElementInfo();
mount(LegendView({ legend: stage.legend, onClick: config && config.onLegendClick }), this.getElement(PresenterElement.legend));
setActiveElement(a);
if (config && config.onPresent) {
config.onPresent();
}
}
public canvasToDataURL() {
return new Promise<string>((resolve, reject) => {
this.morphchartsref.core.afterRenderCallback = () => {
this.morphchartsref.core.afterRenderCallback = null;
const canvas = this.getElement(PresenterElement.gl).getElementsByTagName('canvas')[0];
const png = canvas.toDataURL('image/png');
resolve(png);
};
});
}
public configColors(mcColors: McColors) {
colorConfig(this.morphchartsref, mcColors);
}
/**
* Home the camera to the last initial position.
*/
homeCamera() {
this.morphchartsref?.reset();
}
/**
* Show guidelines of rendering height/width and center of OrbitView.
*/
showGuides() {
this.getElement(PresenterElement.gl).classList.add('show-center');
//TODO Morphcharts gridlines
}
finalize() {
this.animationCancel();
if (this.morphchartsref) this.morphchartsref.core.stop();
if (this.el) this.el.innerHTML = '';
this._last = null;
this.morphchartsref = null;
this.el = null;
this.logger = null;
this.queuedAnimationOptions = null;
}
}

Просмотреть файл

@ -0,0 +1,171 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import legend from './marks/legend';
import image from './marks/image';
import rect from './marks/rect';
import rule, { box } from './marks/rule';
import line from './marks/line';
import text from './marks/text';
import {
Axis,
AxisRole,
FacetRect,
Stage,
StyledLine,
} from './interfaces';
import { base } from './base';
import { colorFromString } from './color';
import { groupStrokeWidth } from './defaults';
import {
GroupType,
MarkStager,
MarkStagerOptions,
} from './marks/interfaces';
import { Axis as VegaSpecAxis, Scene, SceneGroup, SceneItem } from 'vega-typings';
interface VegaAxisDatum {
domain: boolean;
grid: boolean;
labels: boolean;
ticks: boolean;
title: boolean;
}
interface VegaAxisExtend {
plane?: string;
}
//TODO add to vega-typings
interface SceneAxis {
mark: {
zindex?: number;
};
orient?: 'bottom' | 'top' | 'left' | 'right';
}
function getAxisGroupType(item: SceneItem, options: MarkStagerOptions): GroupType {
const axisItem = item as SceneAxis;
const axisMark = axisItem?.mark;
if (axisMark?.zindex === options.zAxisZindex && options.zAxisZindex !== undefined) {
return GroupType.zAxis;
}
switch (axisItem.orient) {
case 'bottom':
case 'top':
return GroupType.xAxis;
case 'left':
case 'right':
return GroupType.yAxis;
}
}
function convertGroupRole(item: SceneItem, options: MarkStagerOptions): GroupType {
if (item.mark.role === 'legend') return GroupType.legend;
if (item.mark.role === 'axis') {
const groupType = getAxisGroupType(item, options);
if (groupType !== undefined) {
return groupType;
}
}
}
const group: MarkStager = (options: MarkStagerOptions, stage: Stage, scene: Scene, x: number, y: number, groupType: GroupType) => {
base.vega.sceneVisit(scene, function (g: SceneGroup) {
const gx = g.x || 0, gy = g.y || 0;
if (g.context && g.context.background && !stage.backgroundColor) {
stage.backgroundColor = colorFromString(g.context.background);
}
if (g.stroke) {
const facetRect: FacetRect = {
datum: g.datum,
lines: box(gx + x, gy + y, g.height, g.width, g.stroke, groupStrokeWidth),
};
stage.facets.push(facetRect);
}
groupType = convertGroupRole(g, options) || groupType;
setCurrentAxis(options, stage, groupType);
// draw group contents
base.vega.sceneVisit(g, function (item: Scene) {
mainStager(options, stage, item, gx + x, gy + y, groupType);
});
});
};
function setCurrentAxis(options: MarkStagerOptions, stage: Stage, groupType: GroupType) {
let axisRole: AxisRole;
switch (groupType) {
case GroupType.xAxis:
axisRole = 'x';
break;
case GroupType.yAxis:
axisRole = 'y';
break;
case GroupType.zAxis:
axisRole = 'z';
break;
default:
return;
}
options.currAxis = {
axisRole,
domain: null,
tickText: [],
ticks: [],
};
stage.axes[axisRole].push(options.currAxis);
}
const markStagers: { [id: string]: MarkStager } = {
group,
legend,
image,
rect,
rule,
line,
text,
};
const mainStager: MarkStager = (options: MarkStagerOptions, stage: Stage, scene: Scene, x: number, y: number, groupType: GroupType) => {
if (scene.marktype !== 'group' && groupType === GroupType.legend) {
legend(options, stage, scene, x, y, groupType);
} else {
const markStager = markStagers[scene.marktype];
if (markStager) {
markStager(options, stage, scene, x, y, groupType);
} else {
//console.log(`need to render ${scene.marktype}`);
}
}
};
export function sceneToStage(options: MarkStagerOptions, stage: Stage, scene: Scene) {
mainStager(options, stage, scene, 0, 0, null);
sortAxis(stage.axes.x, 0);
sortAxis(stage.axes.y, 1);
sortAxis(stage.axes.z, 2);
}
function sortAxis(axes: Axis[], dim: number) {
axes.forEach(axis => {
if (axis.domain) orderDomain(axis.domain, dim);
axis.ticks.sort((a, b) => a.sourcePosition[dim] - b.sourcePosition[dim]);
axis.tickText.sort((a, b) => a.position[dim] - b.position[dim]);
});
}
function orderDomain(domain: StyledLine, dim: number) {
if (domain.sourcePosition[dim] > domain.targetPosition[dim]) {
const temp = domain.targetPosition;
domain.targetPosition = domain.sourcePosition;
domain.sourcePosition = temp;
}
}

Просмотреть файл

@ -0,0 +1,81 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { base } from '../base';
import { Presenter } from '../presenter';
import { PresenterConfig, Scene3d } from '../interfaces';
import { Renderer, Scene, SceneItem } from 'vega-typings';
import { View } from '@msrvida/chart-types';
//pass in the SuperClass, which should be a vega.View
function _RendererGl(loader?: any) {
//dynamic superclass, since we don't know have vega.View in the declaration phase
class RendererGlInternal extends base.vega.Renderer {
public height: number;
public width: number;
public origin: number[];
public presenter: Presenter;
public presenterConfig: PresenterConfig;
public getView: { (): View };
initialize(el, width, height, origin) {
this.height = height;
this.width = width;
// this method will invoke resize to size the canvas appropriately
return super.initialize(el, width, height, origin);
}
resize(width, height, origin) {
super.resize(width, height, origin);
this.origin = origin;
this.height = height;
this.width = width;
//rteturn this for vega
return this;
}
_render(scene: Scene, items: SceneItem[]) {
const scene3d = scene as Scene3d;
scene3d.view = this.getView();
this.presenter.present(scene3d, this.height, this.width, this.presenterConfig);
//return this for vega
return this;
}
}
const instance = new RendererGlInternal(loader) as Renderer;
return instance;
}
//signature to allow this function to be used with the 'new' keyword.
//need to trick the compiler by casting to 'any'.
/**
* Subclass of Vega.Renderer, with added properties for accessing a Presenter.
* This is instantiated by ViewGl.
*/
export const RendererGl: typeof RendererGl_Class = _RendererGl as any;
/**
* Subclass of Vega.Renderer, with added properties for accessing a Presenter.
* This is not instantiatable, it is the TypeScript declaration of the type.
*/
export declare class RendererGl_Class extends base.vega.Renderer {
public height: number;
public width: number;
public origin: number[];
public presenter: Presenter;
public presenterConfig: PresenterConfig;
public getView: { (): View };
}

Просмотреть файл

@ -0,0 +1,127 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
import { base } from '../base';
import { defaultView } from '../defaults';
import { Presenter } from '../presenter';
import { PresenterConfig } from '../interfaces';
import { PresenterElement } from '../enums';
import { RendererGl, RendererGl_Class } from './rendererGl';
import { Renderers, Runtime, View as VegaView, Color, Loader, LoggerInterface, TooltipHandler, LocaleFormatters } from 'vega-typings';
import { View } from '@msrvida/chart-types';
let registered = false;
/**
* ViewOptions from vNext vega-typings https://github.com/vega/vega/pull/2963
*/
export interface ViewOptions {
background?: Color;
bind?: Element | string;
container?: Element | string;
hover?: boolean;
loader?: Loader;
logger?: LoggerInterface;
logLevel?: number;
renderer?: Renderers;
tooltip?: TooltipHandler;
locale?: LocaleFormatters;
expr?: any;
}
/**
* Options for the View.
*/
export interface ViewGlConfig extends ViewOptions {
presenter?: Presenter;
presenterConfig?: PresenterConfig;
getView?: { (): View };
}
//dynamic superclass lets us create a subclass at execution phase instead of declaration phase.
//This allows us to retrieve vega.View from either UMD or ES6 consumers of this class.
//pass in the SuperClass, which should be a vega.View
function _ViewGl(runtime: Runtime, config?: ViewGlConfig) {
//dynamic superclass, since we don't know have vega.View in the declaration phase
class ViewGlInternal extends base.vega.View {
public presenter: Presenter;
constructor(runtime: Runtime, private config: ViewGlConfig = {}) {
super(runtime, config);
this.presenter = config.presenter;
config.presenterConfig = config.presenterConfig || {};
config.presenterConfig.redraw = () => {
(this as any)._redraw = true; //use Vega View private member _redraw
this.run();
};
}
renderer(): Renderers;
renderer(renderer: Renderers | 'morphcharts'): this;
renderer(...args: any[]) {
if (args && args.length) {
const renderer:Renderers | 'morphcharts' = args[0];
if (renderer === 'morphcharts' && !registered) {
base.vega.renderModule('morphcharts', { handler: base.vega.CanvasHandler, renderer: RendererGl });
registered = true;
}
return super.renderer(renderer as unknown as Renderers);
} else {
return super.renderer() as Renderers;
}
}
initialize(el: HTMLElement) {
if (!this.presenter) {
this.presenter = new Presenter(el);
}
super.initialize(this.presenter.getElement(PresenterElement.vegaControls));
const renderer = (this as any as ViewGl_Class)._renderer;
renderer.presenterConfig = this.config.presenterConfig;
renderer.presenter = this.presenter;
renderer.getView = this.config && this.config.getView || (() => this.presenter.view || defaultView);
return this;
}
error(e: Error) {
if (this.presenter!.logger) {
this.presenter.logger(e);
}
}
}
const instance = new ViewGlInternal(runtime, config) as VegaView;
return instance;
}
//signature to allow this function to be used with the 'new' keyword.
//need to trick the compiler by casting to 'any'.
/**
* Subclass of Vega.View, with added properties for accessing a Presenter.
* This is instantiatable by calling `new ViewGl()`. See https://vega.github.io/vega/docs/api/view/
*/
export const ViewGl: typeof ViewGl_Class = _ViewGl as any;
/**
* Subclass of Vega.View, with added properties for accessing a Presenter.
* This is not instantiatable, it is the TypeScript declaration of the type.
*/
export declare class ViewGl_Class extends base.vega.View {
public presenter: Presenter;
constructor(runtime: Runtime, config?: ViewGlConfig);
renderer(renderer: Renderers | 'morphcharts'): this;
renderer(): Renderers;
_renderer: RendererGl_Class;
}

Просмотреть файл

@ -0,0 +1,6 @@
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
export const version: string = 'DEBUG';

Просмотреть файл

@ -0,0 +1,14 @@
{
"compilerOptions": {
"declaration": true,
"jsx": "react",
"jsxFactory": "createElement",
"moduleResolution": "node",
"outDir": "dist/es6",
"skipLibCheck": true,
"target": "es6"
},
"include": [
"vegaspec"
]
}

Просмотреть файл

@ -0,0 +1,95 @@
html,
body {
font-family: sans-serif;
height: 100%;
}
body.rows {
display: grid;
grid-template-rows: 100px auto 1em;
grid-template-areas: "header" "view" "footer";
margin: 0;
overflow: scroll;
}
body.columns {
display: grid;
grid-template-rows: 100px auto 1em;
grid-template-columns: 40% 60%;
grid-template-areas: "header header" "left right" "footer footer";
margin: 0;
}
header {
border-bottom: 1px solid silver;
grid-area: header;
padding: 0 2em;
}
footer {
border-top: 1px solid silver;
grid-area: footer;
padding: 0 2em;
}
#view-type-button {
position: absolute;
right: 2em;
top: 2em;
}
#view-div, #view-div .vega-morphcharts-gl {
height: 100%;
}
#split-left {
grid-area: left;
overflow: hidden;
}
#vis {
grid-area: right;
position: relative;
}
.textform {
background-color: #ccc;
display: grid;
grid-template-rows: auto 2em;
height: 100%;
}
#split-left textarea {
border: 0;
height: 100%;
padding: 0 0 0 5px;
resize: none;
}
#split-right,
#error {
grid-area: right;
}
#error {
font-size: larger;
padding: 1em;
}
#split-right .vega-bindings {
padding: 1em;
}
.vega-morphcharts-root {
display: grid;
grid-template-rows: auto 200px;
height: 100%;
}
.vega-morphcharts-legend {
font-family: sans-serif;
position: absolute;
right: 1em;
top: 1em;
z-index: 1;
}

Просмотреть файл

@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>vega-morphcharts test</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="vega-morphcharts.test.css" />
</head>
<body class="columns">
<header>
<h1>vega-morphcharts spec editor</h1>
<button id="view-type-button" onclick="vegaTest.specRenderer.toggleView()">2D / 3D</button>
</header>
<div id="split-left">
<div class="textform">
<textarea>
{
"$schema": "https://vega.github.io/schema/vega/v4.json",
"width": 500,
"height": 200,
"padding": 5,
"data": [
{
"name": "table",
"values": [
{ "x": 0, "y": 28, "c": 0 }, { "x": 0, "y": 55, "c": 1 },
{ "x": 1, "y": 43, "c": 0 }, { "x": 1, "y": 91, "c": 1 },
{ "x": 2, "y": 81, "c": 0 }, { "x": 2, "y": 53, "c": 1 },
{ "x": 3, "y": 19, "c": 0 }, { "x": 3, "y": 87, "c": 1 },
{ "x": 4, "y": 52, "c": 0 }, { "x": 4, "y": 48, "c": 1 },
{ "x": 5, "y": 24, "c": 0 }, { "x": 5, "y": 49, "c": 1 },
{ "x": 6, "y": 87, "c": 0 }, { "x": 6, "y": 66, "c": 1 },
{ "x": 7, "y": 17, "c": 0 }, { "x": 7, "y": 27, "c": 1 },
{ "x": 8, "y": 68, "c": 0 }, { "x": 8, "y": 16, "c": 1 },
{ "x": 9, "y": 49, "c": 0 }, { "x": 9, "y": 15, "c": 1 }
],
"transform": [
{
"type": "stack",
"groupby": ["x"],
"sort": { "field": "c" },
"field": "y"
}
]
}
],
"scales": [
{
"name": "x",
"type": "band",
"range": "width",
"domain": { "data": "table", "field": "x" }
},
{
"name": "y",
"type": "linear",
"range": "height",
"nice": true, "zero": true,
"domain": { "data": "table", "field": "y1" }
},
{
"name": "color",
"type": "ordinal",
"range": "category",
"domain": { "data": "table", "field": "c" }
}
],
"axes": [
{ "orient": "bottom", "scale": "x", "title": "X Axis", "tickColor": "red", "tickWidth": 3, "labelColor": "blue", "titleColor": "green" },
{ "orient": "left", "scale": "y", "title": "Y Axis", "domainColor": "magenta", "domainWidth": 2, "tickWidth": 7 }
],
"marks": [
{
"type": "rect",
"from": { "data": "table" },
"encode": {
"enter": {
"x": { "scale": "x", "field": "x" },
"width": { "scale": "x", "band": 1, "offset": -1 },
"y": { "scale": "y", "field": "y0" },
"y2": { "scale": "y", "field": "y1" },
"fill": { "scale": "color", "field": "c" }
},
"update": {
"fillOpacity": { "value": 1 }
},
"hover": {
"fillOpacity": { "value": 0.5 }
}
}
}
],
"legends": [
{
"fill": "color",
"title": "Legend",
"encode": {
"symbols": {
"update": {
"shape": { "value": "square" }
}
}
}
}
]
}
</textarea>
<button onclick="vegaTest.specRenderer.getText()">Apply this spec</button>
</div>
</div>
<div id="vis"></div>
<div id="error"></div>
<script type="module" src="vega-morphcharts.test.ts"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import * as vega from 'vega';
import * as VegaMorphCharts from '../../src/index';
VegaMorphCharts.use(vega);
class SpecRenderer {
viewType = '2d';
spec = null;
view = null;
constructor() {
const json = localStorage.getItem('spec');
if (json) {
this.getTextArea().value = json;
}
}
public toggleView() {
if (this.viewType === '3d') {
this.viewType = '2d';
} else {
this.viewType = '3d';
}
this.getText();
}
public getTextArea() {
return <HTMLTextAreaElement>document.getElementsByTagName('textarea')[0];
}
public getText() {
var textarea = this.getTextArea();
var text = textarea.value;
var errorDiv = document.getElementById('error');
var splitRight = document.getElementById('vis');
try {
var spec = JSON.parse(text);
splitRight.style.opacity = '1';
errorDiv.style.display = 'none';
this.update(spec, text);
}
catch (e) {
errorDiv.innerText = e;
errorDiv.style.display = '';
splitRight.style.opacity = '0.1';
}
}
public update(spec: any, json: string) {
// stash the view
if (this.view != null) {
//const deckglviewstate = this.view.presenter.deckgl.viewState;
}
const runtime = vega.parse(spec);
//save in local storage
localStorage.setItem('spec', json);
this.view = new VegaMorphCharts.ViewGl(
runtime,
{
getView: () => {
return this.viewType as any
},
presenterConfig: {
onTargetViewState: (height, width) => {
return { height, width, newViewStateTarget: false };
}
}
})
.renderer('morphcharts')
.initialize(document.querySelector('#vis'));
this.view.run();
}
}
const specRenderer = new SpecRenderer();
window['vegaTest'] = {
vega,
specRenderer,
};

Просмотреть файл

@ -0,0 +1,18 @@
{
"compilerOptions": {
"declaration": true,
"jsx": "react",
"jsxFactory": "createElement",
"lib": [
"DOM",
"ES2019"
],
"moduleResolution": "node",
"outDir": "dist/es6",
"skipLibCheck": true,
"target": "es6"
},
"include": [
"src"
]
}