...
 
Commits (12)
......@@ -71,3 +71,16 @@ sources:
name: "id"
description: "Seller id"
dataType: "integer"
transformers:
#Usar lista de metricas e dimensões para casar com as visões existentes
#Criar um alias/target
-
metrics:
"met:seller:avg:age":
- ":age"
dimensions:
-
name: "dim:seller:name"
cases:
- "name == 'BETO' : beto"
- "name"
......@@ -58,7 +58,7 @@
name: "Seller.id"
description: "id from seller"
dataType: "integer"
-
-
name: "Quantity"
description: "How many is being bought"
dataType: "integer"
......
......@@ -23,6 +23,7 @@ import { View } from "../core/view";
import { Source } from "../core/source";
import { FilterOperator } from "../core/filter";
import { DataType } from "../common/types";
import { each } from "async";
/** @hidden */
const MDB = require("monetdb")();
......@@ -204,4 +205,65 @@ export class MonetAdapter extends SQLAdapter {
return "";
}
}
/**
* Asynchronously sends a message to the database to begin a
* transaction.
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public beginTransaction(cb: (err: Error) => void): void {
this.executeQuery("START TRANSACTION;", cb);
}
/**
* Asynchronously sends a message to the database to commit a
* transaction.
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public commitTransaction(cb: (err: Error) => void): void {
this.executeQuery("COMMIT;", cb);
}
/**
* Asynchronously sends a message to the database to rollback a
* transaction.
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public rollbackTransaction(cb: (err: Error) => void): void {
this.executeQuery("ROLLBACK;", cb);
}
/**
* Asynchronously makes Source data available for reading, using its
* Transformers to update the respective Views.
* WARNING: This function MUST be used inside a transaction
* because possibly inserts and deletes data from multiple sources
* and a fail in the middle of the process WILL cause inconsistence.
* The begin/end of the transaction is not put inside this function
* to give flexibility to the user.
* @param source - "Location" with not "readble" data.
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public useTransformers(source: Source, cb: (err: Error) => void): void {
const queryCopy = source.transformers.map((i) => {
return this.translateTransformer(i, source);
});
queryCopy.push(this.persistSource(source));
each(queryCopy, (query, callback) => {
this.executeQuery(query, callback);
}, (err: Error) => {
if (err) {
cb(err);
return;
}
const query = this.truncSourceSa(source);
this.executeQuery(query, cb);
return;
});
}
}
This diff is collapsed.
......@@ -21,9 +21,10 @@
import { Adapter } from "../core/adapter";
import { Metric } from "../core/metric";
import { Source } from "../core/source";
import { Transformer, Attribute } from "../core/transformer";
import { Dimension } from "../core/dimension";
import { Clause } from "../core/clause";
import { Filter, FilterOperator } from "../core/filter";
import { Filter, FilterOperator, FilterTarget } from "../core/filter";
import { AggregationType, RelationType, DataType } from "../common/types";
import { Operation, Opcode } from "../common/expression";
import { View } from "../core/view";
......@@ -378,7 +379,7 @@ export abstract class SQLAdapter extends Adapter {
* @param item - Attribute to get the name quoted.
* @param viewName - View name used to build the view name.
*/
private buildColumn (item: Metric|Dimension, viewName: string): string {
private buildColumn (item: FilterTarget, viewName: string): string {
const quotedName = "\"" + item.name + "\"";
return viewName + "." + quotedName;
}
......@@ -401,7 +402,7 @@ export abstract class SQLAdapter extends Adapter {
/**
* Translate Filter object into a SQL query condition.
* @param filter - Filter to be translated.
* @param map - Dictionary indexed bu attributes name
* @param map - Dictionary indexed by attributes name
* containing its locations.
*/
private translateFilter(filter: Filter, map: {[key: string]: View}): string {
......@@ -469,7 +470,7 @@ export abstract class SQLAdapter extends Adapter {
for (let i = 0; i < tam; i++){
values[i] = data[colums[i]];
}
consult = "INSERT INTO " + source.name + " (\"";
consult = "INSERT INTO " + source.saName + " (\"";
consult = consult.concat(colums.join("\",\""));
consult = consult.concat("\") VALUES ('");
consult = consult.concat(values.join("' , '"));
......@@ -478,6 +479,73 @@ export abstract class SQLAdapter extends Adapter {
return consult;
}
/**
* Translate a attribute objet to a SQL Case clause
* @param attr - Attribute (list of cases) to be translated
*/
private translateCase(attr: Attribute): string {
const whens = attr.cases.map((i) => {
if (i.condition) {
const filter = i.condition;
const leftSide = "'" + filter.target.name + "'";
const quotedValue = "'" + filter.value + "'";
const castedValue = this.typeCast(quotedValue, filter.target.dataType);
const condition = this.applyOperator(leftSide, castedValue, filter.operator);
return "WHEN " + condition + " THEN " + i.expression;
}
else {
return "ELSE " + i.expression;
}
});
return "CASE " + whens.join(" ") + " END";
}
/**
* Translate a transformer object into a SQL query that
* performs the transformation. It selects all data from
* the source staging area and inserts into the target View.
* @param transformer - The transformer to be translate
* @param source - The data source from the transform will retrieve the data
*/
protected translateTransformer(transformer: Transformer, source: Source): string {
const view = transformer.view;
let attrs = view.metrics.map((i: Metric) => i.name);
attrs = attrs.concat(view.dimensions.map((i: Dimension) => i.name));
const strAttrs = "(" + attrs.join(",") + ")";
let cases = transformer.metrics.map((i) => this.translateCase(i));
cases = cases.concat(transformer.dimensions.map((i) => {
return this.translateCase(i);
}));
const strCases = cases.join(",");
const insertQuery = "INSERT INTO " + view.name + strAttrs;
const selectQuery = "SELECT " + strCases + " FROM" + source.saName;
return insertQuery + " " + selectQuery + ";";
}
/**
* Copy the data from the source staging area to its
* real location.
* @param source - The data source to be persisted
*/
protected persistSource(source: Source): string {
const attrs = source.fields.map((i) => i.name);
const strAttrs = attrs.join(",");
const insertQuery = "INSERT INTO " + source.name + "(" + strAttrs + ")";
const selectQuery = "SELECT " + strAttrs + " FROM" + source.saName;
return insertQuery + " " + selectQuery + ";";
}
/**
* Deletes all data from the source staging area.
* @param source - The source to truncate the staging area
*/
protected truncSourceSa(source: Source): string {
return "DELETE FROM " + source.saName + ";";
}
/**
* Parse a filter operation. Varies in each database.
* @param lSide - Operation left side operator.
......
......@@ -49,4 +49,65 @@ export abstract class Adapter {
* @param cb.result - Query result.
*/
public abstract insertIntoSource(source: Source, data: any[], cb: (err: Error, result: any[]) => void): void;
/**
* Asynchronously makes Source data available for reading, using its
* Transformers to update the respective Views.
* @param source - "Location" with not "readble" data.
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public abstract useTransformers(source: Source, cb: (err: Error) => void): void;
/**
* Asynchronously sends a message to the database to begin a
* transaction.
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public abstract beginTransaction(cb: (err: Error) => void): void;
/**
* Asynchronously sends a message to the database to commit a
* transaction.
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public abstract commitTransaction(cb: (err: Error) => void): void;
/**
* Asynchronously sends a message to the database to rollback a
* transaction.
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public abstract rollbackTransaction(cb: (err: Error) => void): void;
/**
* Asynchronously sends a message to the database to create
* database elements (tables) required to maniputate a view.
* @param view - Element to be initialized on database
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public abstract initView(view: View, cb: (err: Error) => void): void;
/**
* Asynchronously sends a message to the database to create
* database elements (tables) required to maniputate a source.
* @param source - Element to be initialized on database
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public abstract initSource(source: Source, cb: (err: Error) => void): void;
/**
* Asynchronously sends a message to the database to delete
* all tuples stored in the given view.
* @param view - Element to be initialized on database
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public abstract truncateView(view: View, cb: (err: Error) => void): void;
/**
* Asynchronously sends a message to the database to delete
* all tuples stored in the given source (and staging area).
* @param source - Element to be initialized on database
* @param cb - Callback function with operation status.
* @param cb.error - Error information when the method fails.
*/
public abstract truncateSource(source: Source, cb: (err: Error) => void): void;
}
......@@ -18,10 +18,8 @@
* along with blend. If not, see <http://www.gnu.org/licenses/>.
*/
import { Filter } from "./filter";
import { Filter, FilterTarget } from "./filter";
import { Hash } from "../util/hash";
import { Dimension } from "./dimension";
import { Metric } from "./metric";
/** Parameters used to create a Clause object. */
export interface ClauseOptions {
......@@ -36,7 +34,7 @@ export class Clause {
/** Set of filters the form the clause. */
public readonly filters: Filter[];
/** Set of attributes affected by this clause. */
public readonly targets: (Metric|Dimension)[];
public readonly targets: (FilterTarget)[];
/**
* Create a clause object.
......@@ -70,7 +68,7 @@ export class Clause {
* @param coverage - Set of attributes which will be verified if
* contain all targets in the clause.
*/
public isCovered(coverage: (Metric|Dimension)[]): boolean {
public isCovered(coverage: (FilterTarget)[]): boolean {
return this.targets.every((i) => coverage.some((j) => i.name === j.name));
}
}
......@@ -21,7 +21,7 @@
import { Dimension, DimensionStrOptions } from "./dimension";
import { Metric, MetricStrOptions } from "./metric";
import { Clause } from "./clause";
import { Filter } from "./filter";
import { Filter, FilterTarget } from "./filter";
import { View } from "./view";
import { Query, QueryOpts } from "../common/query";
import { Graph } from "../util/graph";
......@@ -232,7 +232,7 @@ export class Engine {
if (segment) {
// Segment never returns NONE
let op = Filter.parseOperator(segment.operator);
let target: Metric|Dimension = null;
let target: FilterTarget = null;
try {
target = this.getDimensionByName(segment.target);
}
......
......@@ -20,13 +20,17 @@
import { Dimension } from "./dimension";
import { Metric } from "./metric";
import { Field } from "./source";
import { Hash } from "../util/hash";
import { DataType } from "../common/types";
/** Allowed types to be target in a filter. */
export type FilterTarget = Metric|Dimension|Field;
/** Parameters used to create a filter object. */
export interface FilterOptions {
/** Metric/Dimension that will be filtered. */
target: Metric|Dimension;
target: FilterTarget;
/** Operation applied to filter. */
operator: FilterOperator;
/** Constant value to be compared. */
......@@ -68,7 +72,7 @@ export class Filter {
/** Hash of components that unique identify the filter. */
public readonly id: string;
/** Metric/Dimension that will be filtered. */
public readonly target: Metric|Dimension;
public readonly target: FilterTarget;
/** Constant value to be compared. */
public readonly operator: FilterOperator;
/** Constant value to be compared. */
......
......@@ -20,6 +20,7 @@
import { DataType } from "../common/types";
import { EnumHandler } from "../util/enumHandler";
import { Transformer, TransformerStrOptions } from "./transformer";
/** Attribute of a source. */
export interface Field {
......@@ -53,6 +54,8 @@ export interface SourceOptions {
description?: string;
/** Set of fields that define the attributes of this source. */
fields: Field[];
/** Set of transformers applied on this source. */
transformers?: Transformer[];
}
/**
......@@ -66,6 +69,19 @@ export interface SourceStrOptions {
description?: string;
/** Set of fields that define the attributes of this source. */
fields: FieldStr[];
/** Set of transformers applied on this source. */
transformers?: TransformerStrOptions[];
}
/**
* Information required to load some data (fixtures) into a source
* to perform tests.
*/
export interface LoadSource {
/** Path to fixture file. */
fixture: string;
/** Source where the fixture data will be inserted. */
source: Source;
}
/**
......@@ -78,10 +94,14 @@ export interface SourceStrOptions {
export class Source {
/** Source name. */
public readonly name: string;
/** Source staging area name. */
public readonly saName: string;
/** Brief description of what this field represent. */
public readonly description: string;
/** Set of fields that define the attributes of this source. */
public readonly fields: Field[];
/** Set of transformers applied on this source. */
public readonly transformers: Transformer[];
/**
* Create a source.
......@@ -89,6 +109,7 @@ export class Source {
*/
constructor(options: SourceOptions) {
this.name = options.name;
this.saName = "sa_" + options.name;
this.description = (options.description) ? options.description : "";
this.fields = options.fields.map((item) => {
return {
......@@ -98,6 +119,15 @@ export class Source {
enumType: (item.enumType) ? item.enumType : ""
};
});
if (options.transformers) {
this.transformers = options.transformers.map((transformer) => {
return transformer;
});
}
else {
this.transformers = [];
}
}
/**
......
/*
* Copyright (C) 2018 Centro de Computacao Cientifica e Software Livre
* Departamento de Informatica - Universidade Federal do Parana
*
* This file is part of blendb.
*
* blendb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* blendb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with blendb. If not, see <http://www.gnu.org/licenses/>.
*/
import { Metric } from "./metric";
import { Dimension } from "./dimension";
import { Filter } from "./filter";
import { View } from "./view";
import { Field } from "./source";
/** CASE/WHEN structure to assign value using a transformer. */
export interface Case {
/** Case condition. If true, expression is executed. */
condition: Filter;
/** Expression which result will be assigned. */
expression: string;
}
/** Defines how a metric/dimension is generated using cases. */
export interface Attribute {
/** Target metric/dimension which will be generated. */
name: Metric|Dimension;
/** Sequence of cases to define the target value. */
cases: Case[];
}
/** Attribute description used in configuration file. */
export interface AttributeStr {
/** Target metric/dimension (name) which will be generated. */
name: string;
/** Sequence of cases to define the target value. */
cases: string[];
}
/** Parameters to create a transformer object. */
export interface TransformerOptions {
/** Target view. What will be generated. */
view: View;
/** Metrics to generated and how to do it. */
metrics: Attribute[];
/** Metrics/dimensions to generated and how to do it. */
dimensions: Attribute[];
}
/** Parameters used to define a transformer object in the configuration file. */
export interface TransformerStrOptions {
/** (Nick)name of the transformer. Used to inform errors only. */
alias: string;
/** Metrics to generated and how to do it. */
metrics: AttributeStr[];
/** Metrics/dimensions to generated and how to do it. */
dimensions: AttributeStr[];
}
/**
* A transformer defines a relationship between a view and a source.
* A transform takes one tuple from a source and "transform" it
* in a tuple of a view. The object store how to perform this
* transformation.
*/
export class Transformer {
/** Target view. What will be generated. */
public readonly view: View;
/** Metrics to generated and how to do it. */
public readonly metrics: Attribute[];
/** Dimensions to generated and how to do it. */
public readonly dimensions: Attribute[];
constructor(options: TransformerOptions) {
this.view = options.view;
this.metrics = options.metrics;
this.dimensions = options.dimensions;
}
/**
* Parses a string into a Case object.
* @param option - Case in string format
* @param fields - List of possible fields usable in case condition
*/
public static parseCase(strCase: string, fields: Field[]): Case {
const splited: string[] = strCase.split("?");
if (splited.length === 1) {
return {
condition: null
, expression: splited[0]
};
}
else {
const segment = Filter.segment(splited[0]);
const filter = new Filter ({
target: fields.find((i) => i.name === segment.target)
, operator : Filter.parseOperator(segment.operator)
, value: segment.value
});
return {
condition: filter
, expression: splited[1]
};
}
}
}
......@@ -24,14 +24,6 @@ import { Hash } from "../util/hash";
import { Clause } from "./clause";
import { Operation, Opcode } from "../common/expression";
/** Information required to load fixture data into the view. */
export interface LoadView {
/** View where the fixture data will be load. */
view: View;
/** Path to a file with fixture data. */
data: string;
}
/** Parameters used to create a view object. */
export interface ViewOptions {
/** Set of metrics contained into the view. */
......@@ -39,7 +31,7 @@ export interface ViewOptions {
/** Set of dimension contained into the view. */
dimensions: Dimension[];
/** Inform if this view cannot be generated by other views. */
origin: boolean;
origin?: boolean;
/** Set of clauses applied to the view. */
clauses?: Clause[];
/** List with the sorting order of the attributes. */
......
......@@ -291,7 +291,7 @@ describe("configParser utility library", () => {
};
let error: boolean = false;
try {
ConfigParser.parseSourceOpts(sourc, enumMap);
ConfigParser.parseSourceOpts(sourc, enumMap, {}, {}, {});
}
catch (e) {
error = true;
......
This diff is collapsed.
......@@ -166,7 +166,7 @@ export class Fixture {
}
}
public loadSource(source: Source[] , cb: (err: Error) => void): void {
public loadSource(sources: Source[] , cb: (err: Error) => void): void {
let client = new MDB({
user: this.config.user,
......@@ -179,34 +179,78 @@ export class Fixture {
client.connect();
series([(callback: (err: Error) => void) => {
each(source, (insere, cback) => {
client.query(this.extractData(insere, true)).then((result: any) => cback())
each(sources, (source, cback) => {
const createQuery = this.createSourceTable(source, true);
client.query(createQuery).then((result: any) => cback())
.fail((errCreate: Error) => {
if (errCreate) {
let regex = new RegExp(/42s01!create table: name \'.*\' already in use/);
if (regex.test(errCreate.message.toLocaleLowerCase())){
return client.query(this.extractData(insere, false)).then((result: any) => cback())
const truncQuery = this.createSourceTable(source, false);
return client.query(truncQuery).then((result: any) => cback())
.fail((errTrunc: Error) => (cback(errTrunc)));
}
}
return cback(errCreate);
});
}, (errQuery: Error) => callback(errQuery));
}, (errQuery: Error) => callback(errQuery));
}, (callback: (err: Error) => void) => {
each(sources, (source, cback) => {
const createQuery = this.createSourceSa(source, true);
client.query(createQuery).then((result: any) => cback())
.fail((errCreate: Error) => {
if (errCreate) {
let regex = new RegExp(/42s01!create table: name \'.*\' already in use/);
if (regex.test(errCreate.message.toLocaleLowerCase())){
const truncQuery = this.createSourceSa(source, false);
return client.query(truncQuery).then((result: any) => cback())
.fail((errTrunc: Error) => (cback(errTrunc)));
}
}
return cback(errCreate);
});
}, (errQuery: Error) => callback(errQuery));
}], (errQuery: Error) => {
client.close();
cb(errQuery);
});
}
private extractData(data: Source, create: boolean): string{
private createSourceTable(source: Source, create: boolean): string {
let name: string;
let type: DataType[];
let fields: string[];
let consult: string;
name = source.name;
type = source.fields.map((item) => item.dataType);
fields = source.fields.map((item) => item.name);
if (create){
consult = "CREATE TABLE " + name + " (" + '"';
for (let i = 0; i < fields.length; i++){
fields[i] = fields[i].concat('"' + " " + this.typeConvertion(type[i]));
}
consult = consult.concat(fields.join(", " + '"'));
consult = consult.concat(");");
}
else{
consult = "DELETE FROM " + name + ";";
}
return consult;
}
private createSourceSa(source: Source, create: boolean): string {
let name: string;
let type: DataType[];
let fields: string[];
let consult: string;
name = data.name;
type = data.fields.map((item) => item.dataType);
fields = data.fields.map((item) => item.name);
name = source.saName;
type = source.fields.map((item) => item.dataType);
fields = source.fields.map((item) => item.name);
if (create){
consult = "CREATE TABLE " + name + " (" + '"';
......
......@@ -19,8 +19,8 @@
*/
import { Client, PoolConfig } from "pg";
import { View, LoadView } from "../../src/core/view";
import { Source } from "../../src/core/source";
import { View } from "../../src/core/view";
import { Source, LoadSource } from "../../src/core/source";
import { each, series } from "async";
import * as fs from "fs";
import { DataType } from "../../src/common/types";
......@@ -52,7 +52,7 @@ export class Fixture {
this.database = config.database;
}
public load(schemas: LoadView[], cb: (err: Error) => void): void {
public load(sources: LoadSource[], views: View[], cb: (err: Error) => void): void {
let client = new Client(this.config);
/*
......@@ -161,7 +161,7 @@ export class Fixture {
}
return init;
}
public loadSource(source: Source[] , cb: (err: Error) => void): void {
public loadSource(sources: Source[] , cb: (err: Error) => void): void {
let client = new Client(this.config);
......@@ -171,12 +171,28 @@ export class Fixture {
return;
}
series([(callback: (err: Error) => void) => {
each(source, (insere, cback) => {
client.query ( this.extractData(insere, true), [], (errCreate: Error) => {
each(sources, (source, cback) => {
const createQuery = this.createSourceTable(source, true);
client.query (createQuery , [], (errCreate: Error) => {
if (errCreate) {
let regex = new RegExp(/relation \".*\" already exists/);
if (regex.test(errCreate.message.toLowerCase())){
return client.query ( this.extractData(insere, false), [], (errTrunc: Error) => cback(errTrunc));
const truncQuery = this.createSourceTable(source, false);
return client.query (truncQuery, [], (errTrunc: Error) => cback(errTrunc));
}
}
return cback(errCreate);
});
}, (errQuery: Error) => callback(errQuery));
}, (callback: (err: Error) => void) => {
each(sources, (source, cback) => {
const createQuery = this.createSourceSa(source, true);
client.query (createQuery , [], (errCreate: Error) => {
if (errCreate) {
let regex = new RegExp(/relation \".*\" already exists/);
if (regex.test(errCreate.message.toLowerCase())){
const truncQuery = this.createSourceSa(source, false);
return client.query (truncQuery, [], (errTrunc: Error) => cback(errTrunc));
}
}
return cback(errCreate);
......@@ -194,15 +210,42 @@ export class Fixture {
});
}
private extractData(data: Source, create: boolean): string{
private createSourceTable(source: Source, create: boolean): string {
let name: string;
let type: DataType[];
let fields: string[];
let consult: string;
name = source.name;
type = source.fields.map((item) => item.dataType);
fields = source.fields.map((item) => item.name);
if (create){
consult = "CREATE TABLE " + name + " (" + '"';
for (let i = 0; i < fields.length; i++){
fields[i] = fields[i].concat('"' + " " + this.typeConvertion(type[i]));
}
consult = consult.concat(fields.join(", " + '"'));
consult = consult.concat(");");
}
else{
consult = "TRUNCATE TABLE " + name + ";";
}
return consult;
}
private createSourceSa(source: Source, create: boolean): string {
let name: string;
let type: DataType[];
let fields: string[];
let consult: string;
name = data.name;
type = data.fields.map((item) => item.dataType);
fields = data.fields.map((item) => item.name);
name = source.saName;
type = source.fields.map((item) => item.dataType);
fields = source.fields.map((item) => item.name);
if (create){
consult = "CREATE TABLE " + name + " (" + '"';
......
[
{"dim:0":"2017-01-01","dim:7":"1","met:0":"1","met:1":"1","met:2":"1","met:10":"1"},
{"dim:0":"2017-01-02","dim:7":"2","met:0":"2","met:1":"2","met:2":"2","met:10":"2"},
{"dim:0":"2017-01-03","dim:7":"3","met:0":"3","met:1":"3","met:2":"3","met:10":"3"},
{"dim:0":"2017-01-04","dim:7":"4","met:0":"4","met:1":"4","met:2":"4","met:10":"4"},
{"dim:0":"2017-01-05","dim:7":"5","met:0":"5","met:1":"5","met:2":"5","met:10":"5"}
]
[
{"dim:1":"2017-01-01","dim:8":"1","met:3":"1","met:4":"1"},
{"dim:1":"2017-01-02","dim:8":"2","met:3":"2","met:4":"2"},
{"dim:1":"2017-01-03","dim:8":"3","met:3":"3","met:4":"3"},
{"dim:1":"2017-01-04","dim:8":"4","met:3":"4","met:4":"4"},
{"dim:1":"2017-01-05","dim:8":"5","met:3":"5","met:4":"5"}
]
[
{"dim:2":"1","met:5":"1","met:6":"1","met:11":"1"},
{"dim:2":"2","met:5":"2","met:6":"2","met:11":"2"},
{"dim:2":"3","met:5":"3","met:6":"3","met:11":"3"},
{"dim:2":"4","met:5":"4","met:6":"4","met:11":"4"},
{"dim:2":"5","met:5":"5","met:6":"5","met:11":"5"}
]
[
{"dim:2":"1","dim:3":"dim:3:1","dim:4":"dim:4:1"},
{"dim:2":"2","dim:3":"dim:3:2","dim:4":"dim:4:2"},
{"dim:2":"3","dim:3":"dim:3:3","dim:4":"dim:4:3"},
{"dim:2":"4","dim:3":"dim:3:4","dim:4":"dim:4:4"},
{"dim:2":"5","dim:3":"dim:3:5","dim:4":"dim:4:5"}
]
[
{"dim:2":"1","dim:7":"1","dim:8":"1"},
{"dim:2":"2","dim:7":"2","dim:8":"2"},
{"dim:2":"3","dim:7":"3","dim:8":"3"},
{"dim:2":"4","dim:7":"4","dim:8":"4"},
{"dim:2":"5","dim:7":"5","dim:8":"5"}
]
[
{"dim:3":"dim:3:1","met:7":"1"},
{"dim:3":"dim:3:2","met:7":"2"},
{"dim:3":"dim:3:3","met:7":"3"},
{"dim:3":"dim:3:4","met:7":"4"},
{"dim:3":"dim:3:5","met:7":"5"}
]
[
{"dim:4":"dim:4:1","met:8":"1"},
{"dim:4":"dim:4:2","met:8":"2"},
{"dim:4":"dim:4:3","met:8":"3"},
{"dim:4":"dim:4:4","met:8":"4"},
{"dim:4":"dim:4:5","met:8":"5"}
]
[
{"dim:4":"dim:4:1","dim:5":"t","dim:6":"1"},
{"dim:4":"dim:4:2","dim:5":"t","dim:6":"2"},
{"dim:4":"dim:4:3","dim:5":"t","dim:6":"3"},
{"dim:4":"dim:4:4","dim:5":"f","dim:6":"4"},
{"dim:4":"dim:4:5","dim:5":"f","dim:6":"5"}
]
[
{"dim:5":"t","dim:6":"1","met:9":"1"},
{"dim:5":"t","dim:6":"2","met:9":"2"},
{"dim:5":"t","dim:6":"3","met:9":"3"},
{"dim:5":"f","dim:6":"4","met:9":"4"},
{"dim:5":"f","dim:6":"5","met:9":"5"}
]
[
{"dim:2":"2","met:5":"2","met:6":"1","met:7":"1"},
{"dim:2":"3","met:5":"3","met:6":"1","met:7":"1"},
{"dim:2":"4","met:5":"4","met:6":"1","met:7":"1"},
{"dim:2":"5","met:5":"5","met:6":"1","met:7":"1"}
]