Update Dart dependencies
Add new dependencies that have been introduced since
update_dart_dependencies was functional
Change-Id: I5b8b5f7a10d9c4c8cb4506cc7c5ddd432716b034
diff --git a/pub/ansicolor/.gitignore b/pub/ansicolor/.gitignore
new file mode 100644
index 0000000..1a90bb8
--- /dev/null
+++ b/pub/ansicolor/.gitignore
@@ -0,0 +1,5 @@
+packages
+pubspec.lock
+.project
+.children
+*~
\ No newline at end of file
diff --git a/pub/ansicolor/AUTHORS b/pub/ansicolor/AUTHORS
new file mode 100644
index 0000000..c3c748b
--- /dev/null
+++ b/pub/ansicolor/AUTHORS
@@ -0,0 +1,4 @@
+Authors:
+
+John Thomas McDole <codefu@google.com>
+
diff --git a/pub/ansicolor/BUILD.gn b/pub/ansicolor/BUILD.gn
new file mode 100644
index 0000000..418f65b
--- /dev/null
+++ b/pub/ansicolor/BUILD.gn
@@ -0,0 +1,14 @@
+# This file is generated by importer.py for ansicolor-0.0.9
+
+import("//build/dart/dart_package.gni")
+
+dart_package("ansicolor") {
+ package_name = "ansicolor"
+
+ source_dir = "lib"
+
+ disable_analysis = true
+
+ deps = [
+ ]
+}
diff --git a/pub/ansicolor/LICENSE b/pub/ansicolor/LICENSE
new file mode 100644
index 0000000..80bb203
--- /dev/null
+++ b/pub/ansicolor/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2013 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/pub/ansicolor/README.md b/pub/ansicolor/README.md
new file mode 100644
index 0000000..270a12d
--- /dev/null
+++ b/pub/ansicolor/README.md
@@ -0,0 +1,29 @@
+ANSI / Xterm 256 color library for Dart
+------
+
+Feel like you're missing some color in your terminal programs? Use AnsiPen to add ANSI color codes to your log messages.
+
+Easy to disable for production, just set `color_disabled = false` and all codes will be empty - no re-writing debug messages.
+
+Example
+------
+Note: Be mindful of contrasting colors. If you set "bright white" foreground and don't adjust the background, you'll have a bad time with lighter terminals.
+
+Foreground to bright white with default background:
+```dart
+AnsiPen pen = new AnsiPen()..white(bold: true);
+print(pen("Bright white foreground") + " this text is default fg/bg");
+```
+
+Background as a peach, foreground as white:
+```dart
+AnsiPen pen = new AnsiPen()..white()..rgb(r: 1.0, g: 0.8, b: 0.2);
+print(pen("White foreground with a peach background"));
+```
+
+Rainbow Demo
+------
+
+If you want a specific color, you can call the `xterm()` with the index listed in the rainbow below. To show the rainbow on your own terminal, just call `print(ansi_demo());` or run src/demo.dart
+
+![alt tag](https://raw.github.com/google/ansicolor-dart/master/ansicolor-dart.png)
diff --git a/pub/ansicolor/lib/ansicolor.dart b/pub/ansicolor/lib/ansicolor.dart
new file mode 100644
index 0000000..772eba7
--- /dev/null
+++ b/pub/ansicolor/lib/ansicolor.dart
@@ -0,0 +1,200 @@
+///
+/// Copyright 2013 Google Inc. All Rights Reserved.
+///
+/// ANSI/XTERM SGR (Select Graphics Rendering) support for 256 colors.
+/// Note: if you're using the dart editor, these won't look right in the
+/// terminal; disable via [color_disabled] or use Eclipse with the Dart and
+/// AnsiConsol plugins!
+///
+library ansicolor;
+
+/**
+ * Globally enable or disable [AnsiPen] settings
+ * Handy for turning on and off embedded colors without commenting out code.
+ */
+bool color_disabled = false;
+
+
+/**
+ * Pen attributes for foreground and background colors.
+ * Use the pen in string interpolation to output ansi codes.
+ * Use [up] in string interpolation to globally reset colors.
+ */
+class AnsiPen {
+
+ /**
+ * Treat a pen instance as a function such that pen("msg") is the same as
+ * pen.write("msg").
+ */
+ call(String msg) => write(msg);
+
+ /**
+ * Allow pen colors to be used in a string.
+ * Note: Once the pen is down, its attributes remain in effect till they are
+ * changed by another pen or [up].
+ */
+ String toString() {
+ if (color_disabled) return "";
+ if (_pen != null) return _pen;
+
+ StringBuffer sb = new StringBuffer();
+ if (_fcolor != null) {
+ sb.write("${ANSI_ESC}38;5;${_fcolor}m");
+ }
+
+ if (_bcolor != null) {
+ sb.write("${ANSI_ESC}48;5;${_bcolor}m");
+ }
+
+ _pen = sb.toString();
+ return _pen;
+ }
+
+ /**
+ * Return control codes to change the terminal colors.
+ */
+ String get down => this.toString();
+
+ /**
+ * Reset all pen attributes in the terminal.
+ */
+ String get up => color_disabled ? "" : ANSI_DEFAULT;
+
+ /**
+ * Write the [msg] with the pen's current settings and then reset all
+ * attributes.
+ */
+ String write(String msg) => "${this}$msg$up";
+
+ black({bool bg: false, bool bold: false}) => _std(0, bold, bg);
+ red({bool bg: false, bool bold: false}) => _std(1, bold, bg);
+ green({bool bg: false, bool bold: false}) => _std(2, bold, bg);
+ yellow({bool bg: false, bool bold: false}) => _std(3, bold, bg);
+ blue({bool bg: false, bool bold: false}) => _std(4, bold, bg);
+ magenta({bool bg: false, bool bold: false}) => _std(5, bold, bg);
+ cyan({bool bg: false, bool bold: false}) => _std(6, bold, bg);
+ white({bool bg: false, bool bold: false}) => _std(7, bold, bg);
+
+ /**
+ * Set the pen color to the rgb value between 0.0..1.0
+ */
+ rgb({r: 1.0, g: 1.0, b: 1.0, bool bg: false}) =>
+ xterm(
+ (r.clamp(0.0, 1.0) * 5).toInt() * 36 +
+ (g.clamp(0.0, 1.0) * 5).toInt() * 6 +
+ (b.clamp(0.0, 1.0) * 5).toInt() + 16,
+ bg: bg);
+
+ /**
+ * See the pen color to a grey scale value between 0.0 and 1.0
+ */
+ gray({level: 1.0, bool bg: false}) =>
+ xterm(232 + (level.clamp(0.0, 1.0) * 23).round(), bg: bg);
+
+ _std(int color, bool bold, bool bg) => xterm(color + (bold ? 8 : 0), bg: bg);
+
+ /**
+ * Directly index the xterm 256 color palette.
+ */
+ xterm(int color, {bool bg: false}) {
+ _pen = null;
+ var c = color.toInt().clamp(0, 256);
+ if (bg) {
+ _bcolor = c;
+ } else {
+ _fcolor = c;
+ }
+ }
+
+ /**
+ * Reset the pen's attributes.
+ */
+ reset() {
+ _pen = null;
+ _bcolor = _fcolor = null;
+ }
+
+ int _fcolor;
+ int _bcolor;
+ String _pen;
+}
+
+
+/**
+ * ANSI Control Sequence Introducer, signals the terminal for new settings.
+ */
+String get ANSI_ESC => color_disabled ? "" : '\x1B[';
+
+
+/**
+ * Reset all colors and options for current SGRs to terminal defaults.
+ */
+String get ANSI_DEFAULT => color_disabled ? "" : "${ANSI_ESC}0m";
+
+
+/**
+ * Defaults the terminal's foreground color without altering the background.
+ * Does not modify [AnsiPen]!
+ */
+String resetForeground() => "${ANSI_ESC}39m";
+
+
+/**
+ * Defaults the terminal's background color without altering the foreground.
+ * Does not modify [AnsiPen]!
+ */
+String resetBackground() => "${ANSI_ESC}49m";
+
+
+/**
+ * Due to missing sprintf(), this is my cheap "%03d".
+ */
+String _toSpace(int i, [int width = 3]) {
+ if (width <= 0 && i == 0) return "";
+ return "${_toSpace(i ~/ 10, --width)}${i % 10}";
+}
+
+/**
+ * Return a reference table for foreground and background colors.
+ */
+String ansi_demo() {
+ StringBuffer sb = new StringBuffer();
+
+ AnsiPen pen = new AnsiPen();
+
+ for (int c = 0; c < 16; c++) {
+ pen..reset()..white(bold: true)..xterm(c, bg: true);
+ sb.write(pen("${_toSpace(c)} "));
+ pen..reset()..xterm(c);
+ sb.write(pen(" ${_toSpace(c)} "));
+ if (c == 7 || c == 15) {
+ sb.write("\n");
+ }
+ }
+
+ for (int r = 0; r < 6; r++) {
+ sb.write("\n");
+ for (int g = 0; g < 6; g++) {
+ for (int b = 0; b < 6; b++) {
+ var c = r*36 + g*6 + b + 16;
+ pen..reset()..rgb(r: r / 5, g: g / 5, b: b / 5, bg: true)..
+ white(bold: true);
+ sb.write(pen(" ${_toSpace(c)} "));
+ pen..reset()..rgb(r: r / 5, g: g / 5, b: b / 5);
+ sb.write(pen(" ${_toSpace(c)} "));
+ }
+ sb.write("\n");
+ }
+ }
+
+ for (int c = 0; c < 24; c++) {
+ if (0 == c % 8) {
+ sb.write("\n");
+ }
+ pen..reset()..gray(level: c / 23, bg: true)..white(bold:true);
+ sb.write(pen(" ${_toSpace(c + 232)} "));
+ pen..reset()..gray(level: c / 23);
+ sb.write(pen(" ${_toSpace(c + 232)} "));
+ }
+ return sb.toString();
+}
diff --git a/pub/ansicolor/pubspec.yaml b/pub/ansicolor/pubspec.yaml
new file mode 100644
index 0000000..b2b4193
--- /dev/null
+++ b/pub/ansicolor/pubspec.yaml
@@ -0,0 +1,7 @@
+name: ansicolor
+version: 0.0.9
+author: John Thomas McDole <codefu@google.com>
+description: An xterm 256 color support library
+homepage: https://github.com/google/ansicolor-dart
+dev_dependencies:
+ unittest: ">=0.6.19"
diff --git a/pub/ansicolor/src/demo.dart b/pub/ansicolor/src/demo.dart
new file mode 100644
index 0000000..c562b2d
--- /dev/null
+++ b/pub/ansicolor/src/demo.dart
@@ -0,0 +1,5 @@
+import "../lib/ansicolor.dart";
+
+void main() {
+ print(ansi_demo());
+}
\ No newline at end of file
diff --git a/pub/bazel_worker/BUILD.gn b/pub/bazel_worker/BUILD.gn
index ba8a0b7..3724e96 100644
--- a/pub/bazel_worker/BUILD.gn
+++ b/pub/bazel_worker/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for bazel_worker-0.1.3
+# This file is generated by importer.py for bazel_worker-0.1.4
import("//build/dart/dart_package.gni")
diff --git a/pub/bazel_worker/CHANGELOG.md b/pub/bazel_worker/CHANGELOG.md
index e42904a..7878fb7 100644
--- a/pub/bazel_worker/CHANGELOG.md
+++ b/pub/bazel_worker/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.1.4
+
+* Added `BazelWorkerDriver` class, which can be used to implement the bazel side
+ of the protocol. This allows you to speak to any process which knows the bazel
+ protocol from your own process.
+* Changed `WorkerConnection#readRequest` to return a `FutureOr<WorkRequest>`
+ instead of dynamic.
+
## 0.1.3
* Add automatic intercepting of print calls and append them to
diff --git a/pub/bazel_worker/e2e_test/lib/async_worker.dart b/pub/bazel_worker/e2e_test/lib/async_worker.dart
new file mode 100644
index 0000000..b8239d3
--- /dev/null
+++ b/pub/bazel_worker/e2e_test/lib/async_worker.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:bazel_worker/bazel_worker.dart';
+
+/// Example worker that just returns in its response all the arguments passed
+/// separated by newlines.
+class ExampleAsyncWorker extends AsyncWorkerLoop {
+ Future<WorkResponse> performRequest(WorkRequest request) async {
+ return new WorkResponse()
+ ..exitCode = 0
+ ..output = request.arguments.join('\n');
+ }
+}
diff --git a/pub/bazel_worker/e2e_test/lib/sync_worker.dart b/pub/bazel_worker/e2e_test/lib/sync_worker.dart
new file mode 100644
index 0000000..7c98dbe
--- /dev/null
+++ b/pub/bazel_worker/e2e_test/lib/sync_worker.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:bazel_worker/bazel_worker.dart';
+
+/// Example worker that just returns in its response all the arguments passed
+/// separated by newlines.
+class ExampleSyncWorker extends SyncWorkerLoop {
+ WorkResponse performRequest(WorkRequest request) {
+ return new WorkResponse()
+ ..exitCode = 0
+ ..output = request.arguments.join('\n');
+ }
+}
diff --git a/pub/bazel_worker/e2e_test/pubspec.yaml b/pub/bazel_worker/e2e_test/pubspec.yaml
new file mode 100644
index 0000000..9ecc48c
--- /dev/null
+++ b/pub/bazel_worker/e2e_test/pubspec.yaml
@@ -0,0 +1,8 @@
+name: e2e_test
+dependencies:
+ bazel_worker:
+ path: ../
+dev_dependencies:
+ cli_util: ^0.0.1
+ path: ^1.4.1
+ test: ^0.12.0
diff --git a/pub/bazel_worker/e2e_test/test/e2e_test.dart b/pub/bazel_worker/e2e_test/test/e2e_test.dart
new file mode 100644
index 0000000..2879219
--- /dev/null
+++ b/pub/bazel_worker/e2e_test/test/e2e_test.dart
@@ -0,0 +1,60 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:bazel_worker/driver.dart';
+import 'package:cli_util/cli_util.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+ var sdkDir = getSdkDir();
+ var dart = p.join(sdkDir.path, 'bin', 'dart');
+ runE2eTestForWorker('sync worker',
+ () => Process.start(dart, [p.join('bin', 'sync_worker.dart')]));
+ runE2eTestForWorker('async worker',
+ () => Process.start(dart, [p.join('bin', 'async_worker.dart')]));
+}
+
+void runE2eTestForWorker(String groupName, SpawnWorker spawnWorker) {
+ BazelWorkerDriver driver;
+ group(groupName, () {
+ setUp(() {
+ driver = new BazelWorkerDriver(spawnWorker);
+ });
+
+ tearDown(() async {
+ await driver.terminateWorkers();
+ });
+
+ test('single work request', () async {
+ await _doRequests(driver, count: 1);
+ });
+
+ test('lots of requests', () async {
+ await _doRequests(driver, count: 1000);
+ });
+ });
+}
+
+/// Runs [count] work requests through [driver], and asserts that they all
+/// completed with the correct response.
+Future _doRequests(BazelWorkerDriver driver, {int count}) async {
+ count ??= 100;
+ var requests = new List.generate(count, (requestNum) {
+ var request = new WorkRequest();
+ request.arguments
+ .addAll(new List.generate(requestNum, (argNum) => '$argNum'));
+ return request;
+ });
+ var responses = await Future.wait(requests.map(driver.doWork));
+ for (int i = 0; i < responses.length; i++) {
+ var request = requests[i];
+ var response = responses[i];
+ expect(response.exitCode, EXIT_CODE_OK);
+ expect(response.output, request.arguments.join('\n'));
+ }
+}
diff --git a/pub/bazel_worker/lib/bazel_worker.dart b/pub/bazel_worker/lib/bazel_worker.dart
index 8d42fb6..0b00408 100644
--- a/pub/bazel_worker/lib/bazel_worker.dart
+++ b/pub/bazel_worker/lib/bazel_worker.dart
@@ -2,10 +2,10 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-export 'src/async_worker_loop.dart';
+export 'src/worker/async_worker_loop.dart';
export 'src/constants.dart';
export 'src/message_grouper.dart';
-export 'src/sync_worker_loop.dart';
-export 'src/worker_connection.dart';
-export 'src/worker_loop.dart';
+export 'src/worker/sync_worker_loop.dart';
+export 'src/worker/worker_connection.dart';
+export 'src/worker/worker_loop.dart';
export 'src/worker_protocol.pb.dart';
diff --git a/pub/bazel_worker/lib/driver.dart b/pub/bazel_worker/lib/driver.dart
new file mode 100644
index 0000000..7453491
--- /dev/null
+++ b/pub/bazel_worker/lib/driver.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/driver/driver.dart';
+export 'src/driver/driver_connection.dart';
+export 'src/constants.dart';
+export 'src/message_grouper.dart';
+export 'src/worker_protocol.pb.dart';
diff --git a/pub/bazel_worker/lib/src/async_worker_loop.dart b/pub/bazel_worker/lib/src/async_worker_loop.dart
deleted file mode 100644
index bf6c893..0000000
--- a/pub/bazel_worker/lib/src/async_worker_loop.dart
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'dart:async';
-import 'dart:io';
-
-import 'constants.dart';
-import 'async_message_grouper.dart';
-import 'utils.dart';
-import 'worker_connection.dart';
-import 'worker_loop.dart';
-import 'worker_protocol.pb.dart';
-
-/// Connection between a worker and input / output.
-abstract class AsyncWorkerConnection implements WorkerConnection {
- /// Read a new [WorkRequest]. Returns [null] when there are no more requests.
- Future<WorkRequest> readRequest();
-
- /// Write the given [response] as bytes to the output.
- void writeResponse(WorkResponse response);
-}
-
-/// Persistent Bazel worker loop.
-///
-/// Extend this class and implement the `performRequest` method.
-abstract class AsyncWorkerLoop implements WorkerLoop {
- final AsyncWorkerConnection connection;
-
- AsyncWorkerLoop({AsyncWorkerConnection connection})
- : this.connection = connection ?? new StdAsyncWorkerConnection();
-
- /// Perform a single [WorkRequest], and return a [WorkResponse].
- Future<WorkResponse> performRequest(WorkRequest request);
-
- /// Run the worker loop. The returned [Future] doesn't complete until
- /// [connection#readRequest] returns `null`.
- Future run() async {
- while (true) {
- WorkResponse response;
- try {
- var request = await connection.readRequest();
- if (request == null) break;
- var printMessages = new StringBuffer();
- response = await runZoned(() => performRequest(request),
- zoneSpecification:
- new ZoneSpecification(print: (self, parent, zone, message) {
- printMessages.writeln();
- printMessages.write(message);
- }));
- if (printMessages.isNotEmpty) {
- response.output = '${response.output}$printMessages';
- }
- // In case they forget to set this.
- response.exitCode ??= EXIT_CODE_OK;
- } catch (e, s) {
- response = new WorkResponse()
- ..exitCode = EXIT_CODE_ERROR
- ..output = '$e\n$s';
- }
-
- connection.writeResponse(response);
- }
- }
-}
-
-/// Default implementation of [AsyncWorkerConnection] that works with [Stdin]
-/// and [Stdout].
-class StdAsyncWorkerConnection implements AsyncWorkerConnection {
- final AsyncMessageGrouper _messageGrouper;
- final StreamSink<List<int>> _outputStream;
-
- StdAsyncWorkerConnection(
- {Stream<List<int>> inputStream, StreamSink<List<int>> outputStream})
- : _messageGrouper = new AsyncMessageGrouper(inputStream ?? stdin),
- _outputStream = outputStream ?? stdout;
-
- @override
- Future<WorkRequest> readRequest() async {
- var buffer = await _messageGrouper.next;
- if (buffer == null) return null;
-
- return new WorkRequest.fromBuffer(buffer);
- }
-
- @override
- void writeResponse(WorkResponse response) {
- _outputStream.add(protoToDelimitedBuffer(response));
- }
-}
diff --git a/pub/bazel_worker/lib/src/driver/driver.dart b/pub/bazel_worker/lib/src/driver/driver.dart
new file mode 100644
index 0000000..9d0bb85
--- /dev/null
+++ b/pub/bazel_worker/lib/src/driver/driver.dart
@@ -0,0 +1,153 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:io';
+
+import '../constants.dart';
+import '../worker_protocol.pb.dart';
+import 'driver_connection.dart';
+
+typedef Future<Process> SpawnWorker();
+
+/// A driver for talking to a bazel worker.
+///
+/// This allows you to use any binary that supports the bazel worker protocol in
+/// the same way that bazel would, but from another dart process instead.
+class BazelWorkerDriver {
+ /// The maximum number of idle workers at any given time.
+ final int _maxIdleWorkers;
+
+ /// The maximum number of concurrent workers to run at any given time.
+ final int _maxWorkers;
+
+ /// The number of currently active workers.
+ int get _numWorkers => _readyWorkers.length + _spawningWorkers.length;
+
+ /// Idle worker processes.
+ final _idleWorkers = <Process>[];
+
+ /// All workers that are fully spawned and ready to handle work.
+ final _readyWorkers = <Process>[];
+
+ /// All workers that are in the process of being spawned.
+ final _spawningWorkers = <Future<Process>>[];
+
+ /// Work requests that haven't been started yet.
+ final _workQueue = new Queue<WorkRequest>();
+
+ /// Factory method that spawns a worker process.
+ final SpawnWorker _spawnWorker;
+
+ BazelWorkerDriver(this._spawnWorker, {int maxIdleWorkers, int maxWorkers})
+ : this._maxIdleWorkers = maxIdleWorkers ?? 4,
+ this._maxWorkers = maxWorkers ?? 4;
+
+ Future<WorkResponse> doWork(WorkRequest request) {
+ var responseCompleter = new Completer<WorkResponse>();
+ _responseCompleters[request] = responseCompleter;
+ _workQueue.add(request);
+ _runWorkQueue();
+ return responseCompleter.future;
+ }
+
+ /// Calls `kill` on all worker processes.
+ Future terminateWorkers() async {
+ for (var worker in _readyWorkers) {
+ worker.kill();
+ }
+ await Future.wait(_spawningWorkers.map((worker) async {
+ (await worker).kill();
+ }));
+ }
+
+ /// Runs as many items in [_workQueue] as possible given the number of
+ /// available workers.
+ ///
+ /// Will spawn additional workers until [_maxWorkers] has been reached.
+ ///
+ /// This method synchronously drains the [_workQueue] and [_idleWorkers], but
+ /// some tasks may not actually start right away if they need to wait for a
+ /// worker to spin up.
+ void _runWorkQueue() {
+ // Bail out conditions, we will continue to call ourselves indefinitely
+ // until one of these is met.
+ if (_workQueue.isEmpty) return;
+ if (_numWorkers == _maxWorkers && _idleWorkers.isEmpty) return;
+ if (_numWorkers > _maxWorkers) {
+ throw new StateError('Internal error, created to many workers. Please '
+ 'file a bug at https://github.com/dart-lang/bazel_worker/issues/new');
+ }
+
+ // At this point we definitely want to run a task, we just need to decide
+ // whether or not we need to start up a new worker.
+ var request = _workQueue.removeFirst();
+ if (_idleWorkers.isNotEmpty) {
+ _runWorker(_idleWorkers.removeLast(), request);
+ } else {
+ // No need to block here, we want to continue to synchronously drain the
+ // work queue.
+ var futureWorker = _spawnWorker();
+ _spawningWorkers.add(futureWorker);
+ futureWorker.then((worker) {
+ _spawningWorkers.remove(futureWorker);
+ _readyWorkers.add(worker);
+
+ _workerConnections[worker] = new StdDriverConnection.forWorker(worker);
+ _runWorker(worker, request);
+
+ // When the worker exits we should retry running the work queue in case
+ // there is more work to be done. This is primarily just a defensive
+ // thing but is cheap to do.
+ worker.exitCode.then((_) {
+ _readyWorkers.remove(worker);
+ _runWorkQueue();
+ });
+ });
+ }
+ // Recursively calls itself until one of the bail out conditions are met.
+ _runWorkQueue();
+ }
+
+ /// Sends [request] to [worker].
+ ///
+ /// Once the worker responds then it will be added back to the pool of idle
+ /// workers.
+ Future _runWorker(Process worker, WorkRequest request) async {
+ try {
+ var connection = _workerConnections[worker];
+ connection.writeRequest(request);
+ var response = await connection.readResponse();
+ _responseCompleters[request].complete(response);
+
+ // Do additional work if available.
+ _idleWorkers.add(worker);
+ _runWorkQueue();
+
+ // If the worker wasn't immediately used we might have to many idle
+ // workers now, kill one if necessary.
+ if (_idleWorkers.length > _maxIdleWorkers) {
+ // Note that whenever we spawn a worker we listen for its exit code
+ // and clean it up so we don't need to do that here.
+ var worker = _idleWorkers.removeLast();
+ _readyWorkers.remove(worker);
+ worker.kill();
+ }
+ } catch (e, s) {
+ // Note that we don't need to do additional cleanup here on failures. If
+ // the worker dies that is already handled in a generic fashion, we just
+ // need to make sure we complete with a valid response.
+ if (!_responseCompleters[request].isCompleted) {
+ var response = new WorkResponse()
+ ..exitCode = EXIT_CODE_ERROR
+ ..output = 'Error running worker:\n$e\n$s';
+ _responseCompleters[request].complete(response);
+ }
+ }
+ }
+}
+
+final _responseCompleters = new Expando<Completer<WorkResponse>>('response');
+final _workerConnections = new Expando<DriverConnection>('connectin');
diff --git a/pub/bazel_worker/lib/src/driver/driver_connection.dart b/pub/bazel_worker/lib/src/driver/driver_connection.dart
new file mode 100644
index 0000000..b170586
--- /dev/null
+++ b/pub/bazel_worker/lib/src/driver/driver_connection.dart
@@ -0,0 +1,75 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import '../async_message_grouper.dart';
+import '../worker_protocol.pb.dart';
+import '../constants.dart';
+import '../utils.dart';
+
+/// A connection from a `BazelWorkerDriver` to a worker.
+///
+/// Unlike `WorkerConnection` there is no synchronous version of this class.
+/// This is because drivers talk to multiple workers, so they should never block
+/// when waiting for the response of any individual worker.
+abstract class DriverConnection {
+ Future<WorkResponse> readResponse();
+
+ void writeRequest(WorkRequest request);
+}
+
+/// Default implementation of [DriverConnection] that works with [Stdin]
+/// and [Stdout].
+class StdDriverConnection implements DriverConnection {
+ final AsyncMessageGrouper _messageGrouper;
+ final StreamSink<List<int>> _outputStream;
+
+ StdDriverConnection(
+ {Stream<List<int>> inputStream, StreamSink<List<int>> outputStream})
+ : _messageGrouper = new AsyncMessageGrouper(inputStream ?? stdin),
+ _outputStream = outputStream ?? stdout;
+
+ factory StdDriverConnection.forWorker(Process worker) =>
+ new StdDriverConnection(
+ inputStream: worker.stdout, outputStream: worker.stdin);
+
+ /// Note: This will attempts to recover from invalid proto messages by parsing
+ /// them as strings. This is a common error case for workers (they print a
+ /// message to stdout on accident). This isn't perfect however as it only
+ /// happens if the parsing throws, you can still hang indefinitely if the
+ /// [MessageGrouper] doesn't find what it thinks is the end of a proto
+ /// message.
+ @override
+ Future<WorkResponse> readResponse() async {
+ var buffer = await _messageGrouper.next;
+ if (buffer == null) return null;
+
+ WorkResponse response;
+ try {
+ response = new WorkResponse.fromBuffer(buffer);
+ } catch (_) {
+ try {
+ // Try parsing the message as a string and set that as the output.
+ var output = UTF8.decode(buffer);
+ var response = new WorkResponse()
+ ..exitCode = EXIT_CODE_ERROR
+ ..output = 'Worker sent an invalid response:\n$output';
+ return response;
+ } catch (_) {
+ // Fall back to original exception and rethrow if we fail to parse as
+ // a string.
+ }
+ rethrow;
+ }
+ return response;
+ }
+
+ @override
+ void writeRequest(WorkRequest request) {
+ _outputStream.add(protoToDelimitedBuffer(request));
+ }
+}
diff --git a/pub/bazel_worker/lib/src/sync_worker_loop.dart b/pub/bazel_worker/lib/src/sync_worker_loop.dart
deleted file mode 100644
index a3528a5..0000000
--- a/pub/bazel_worker/lib/src/sync_worker_loop.dart
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-import 'dart:async';
-import 'dart:io';
-
-import 'constants.dart';
-import 'sync_message_grouper.dart';
-import 'utils.dart';
-import 'worker_connection.dart';
-import 'worker_loop.dart';
-import 'worker_protocol.pb.dart';
-
-/// Connection between a worker and input / output.
-abstract class SyncWorkerConnection implements WorkerConnection {
- /// Read a new [WorkRequest]. Returns [null] when there are no more requests.
- WorkRequest readRequest();
-
- /// Write the given [response] as bytes to the output.
- void writeResponse(WorkResponse response);
-}
-
-/// Persistent Bazel worker loop.
-///
-/// Extend this class and implement the `performRequest` method.
-abstract class SyncWorkerLoop implements WorkerLoop {
- final SyncWorkerConnection connection;
-
- SyncWorkerLoop({SyncWorkerConnection connection})
- : this.connection = connection ?? new StdSyncWorkerConnection();
-
- /// Perform a single [WorkRequest], and return a [WorkResponse].
- WorkResponse performRequest(WorkRequest request);
-
- /// Run the worker loop. Blocks until [connection#readRequest] returns `null`.
- void run() {
- while (true) {
- WorkResponse response;
- try {
- var request = connection.readRequest();
- if (request == null) break;
- var printMessages = new StringBuffer();
- response = runZoned(() => performRequest(request), zoneSpecification:
- new ZoneSpecification(print: (self, parent, zone, message) {
- printMessages.writeln();
- printMessages.write(message);
- }));
- if (printMessages.isNotEmpty) {
- response.output = '${response.output}$printMessages';
- }
- // In case they forget to set this.
- response.exitCode ??= EXIT_CODE_OK;
- } catch (e, s) {
- response = new WorkResponse()
- ..exitCode = EXIT_CODE_ERROR
- ..output = '$e\n$s';
- }
-
- connection.writeResponse(response);
- }
- }
-}
-
-/// Default implementation of [SyncWorkerConnection] that works with [Stdin] and
-/// [Stdout].
-class StdSyncWorkerConnection implements SyncWorkerConnection {
- final SyncMessageGrouper _messageGrouper;
- final Stdout _stdoutStream;
-
- StdSyncWorkerConnection({Stdin stdinStream, Stdout stdoutStream})
- : _messageGrouper = new SyncMessageGrouper(stdinStream ?? stdin),
- _stdoutStream = stdoutStream ?? stdout;
-
- @override
- WorkRequest readRequest() {
- var buffer = _messageGrouper.next;
- if (buffer == null) return null;
-
- return new WorkRequest.fromBuffer(buffer);
- }
-
- @override
- void writeResponse(WorkResponse response) {
- _stdoutStream.add(protoToDelimitedBuffer(response));
- }
-}
diff --git a/pub/bazel_worker/lib/src/worker/async_worker_loop.dart b/pub/bazel_worker/lib/src/worker/async_worker_loop.dart
new file mode 100644
index 0000000..29f6ff8
--- /dev/null
+++ b/pub/bazel_worker/lib/src/worker/async_worker_loop.dart
@@ -0,0 +1,53 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import '../constants.dart';
+import '../worker_protocol.pb.dart';
+import 'worker_connection.dart';
+import 'worker_loop.dart';
+
+/// Persistent Bazel worker loop.
+///
+/// Extend this class and implement the `performRequest` method.
+abstract class AsyncWorkerLoop implements WorkerLoop {
+ final AsyncWorkerConnection connection;
+
+ AsyncWorkerLoop({AsyncWorkerConnection connection})
+ : this.connection = connection ?? new StdAsyncWorkerConnection();
+
+ /// Perform a single [WorkRequest], and return a [WorkResponse].
+ Future<WorkResponse> performRequest(WorkRequest request);
+
+ /// Run the worker loop. The returned [Future] doesn't complete until
+ /// [connection#readRequest] returns `null`.
+ Future run() async {
+ while (true) {
+ WorkResponse response;
+ try {
+ var request = await connection.readRequest();
+ if (request == null) break;
+ var printMessages = new StringBuffer();
+ response = await runZoned(() => performRequest(request),
+ zoneSpecification:
+ new ZoneSpecification(print: (self, parent, zone, message) {
+ printMessages.writeln();
+ printMessages.write(message);
+ }));
+ if (printMessages.isNotEmpty) {
+ response.output = '${response.output}$printMessages';
+ }
+ // In case they forget to set this.
+ response.exitCode ??= EXIT_CODE_OK;
+ } catch (e, s) {
+ response = new WorkResponse()
+ ..exitCode = EXIT_CODE_ERROR
+ ..output = '$e\n$s';
+ }
+
+ connection.writeResponse(response);
+ }
+ }
+}
diff --git a/pub/bazel_worker/lib/src/worker/sync_worker_loop.dart b/pub/bazel_worker/lib/src/worker/sync_worker_loop.dart
new file mode 100644
index 0000000..e1b9aaf
--- /dev/null
+++ b/pub/bazel_worker/lib/src/worker/sync_worker_loop.dart
@@ -0,0 +1,50 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+import 'dart:async';
+
+import '../constants.dart';
+import '../worker_protocol.pb.dart';
+import 'worker_connection.dart';
+import 'worker_loop.dart';
+
+/// Persistent Bazel worker loop.
+///
+/// Extend this class and implement the `performRequest` method.
+abstract class SyncWorkerLoop implements WorkerLoop {
+ final SyncWorkerConnection connection;
+
+ SyncWorkerLoop({SyncWorkerConnection connection})
+ : this.connection = connection ?? new StdSyncWorkerConnection();
+
+ /// Perform a single [WorkRequest], and return a [WorkResponse].
+ WorkResponse performRequest(WorkRequest request);
+
+ /// Run the worker loop. Blocks until [connection#readRequest] returns `null`.
+ void run() {
+ while (true) {
+ WorkResponse response;
+ try {
+ var request = connection.readRequest();
+ if (request == null) break;
+ var printMessages = new StringBuffer();
+ response = runZoned(() => performRequest(request), zoneSpecification:
+ new ZoneSpecification(print: (self, parent, zone, message) {
+ printMessages.writeln();
+ printMessages.write(message);
+ }));
+ if (printMessages.isNotEmpty) {
+ response.output = '${response.output}$printMessages';
+ }
+ // In case they forget to set this.
+ response.exitCode ??= EXIT_CODE_OK;
+ } catch (e, s) {
+ response = new WorkResponse()
+ ..exitCode = EXIT_CODE_ERROR
+ ..output = '$e\n$s';
+ }
+
+ connection.writeResponse(response);
+ }
+ }
+}
diff --git a/pub/bazel_worker/lib/src/worker/worker_connection.dart b/pub/bazel_worker/lib/src/worker/worker_connection.dart
new file mode 100644
index 0000000..7fdd4e7
--- /dev/null
+++ b/pub/bazel_worker/lib/src/worker/worker_connection.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import '../async_message_grouper.dart';
+import '../sync_message_grouper.dart';
+import '../utils.dart';
+import '../worker_protocol.pb.dart';
+
+/// A connection from a worker to a driver (driver could be bazel, a dart
+/// program using `BazelWorkerDriver`, or any other process that speaks the
+/// protocol).
+abstract class WorkerConnection {
+ /// Reads a [WorkRequest] or returns [null] if there are none left.
+ ///
+ /// See [AsyncWorkerConnection] and [SyncWorkerConnection] for more narrow
+ /// interfaces.
+ FutureOr<WorkRequest> readRequest();
+
+ void writeResponse(WorkResponse response);
+}
+
+abstract class AsyncWorkerConnection implements WorkerConnection {
+ @override
+ Future<WorkRequest> readRequest();
+}
+
+abstract class SyncWorkerConnection implements WorkerConnection {
+ WorkRequest readRequest();
+}
+
+/// Default implementation of [AsyncWorkerConnection] that works with [Stdin]
+/// and [Stdout].
+class StdAsyncWorkerConnection implements AsyncWorkerConnection {
+ final AsyncMessageGrouper _messageGrouper;
+ final StreamSink<List<int>> _outputStream;
+
+ StdAsyncWorkerConnection(
+ {Stream<List<int>> inputStream, StreamSink<List<int>> outputStream})
+ : _messageGrouper = new AsyncMessageGrouper(inputStream ?? stdin),
+ _outputStream = outputStream ?? stdout;
+
+ @override
+ Future<WorkRequest> readRequest() async {
+ var buffer = await _messageGrouper.next;
+ if (buffer == null) return null;
+
+ return new WorkRequest.fromBuffer(buffer);
+ }
+
+ @override
+ void writeResponse(WorkResponse response) {
+ _outputStream.add(protoToDelimitedBuffer(response));
+ }
+}
+
+/// Default implementation of [SyncWorkerConnection] that works with [Stdin] and
+/// [Stdout].
+class StdSyncWorkerConnection implements SyncWorkerConnection {
+ final SyncMessageGrouper _messageGrouper;
+ final Stdout _stdoutStream;
+
+ StdSyncWorkerConnection({Stdin stdinStream, Stdout stdoutStream})
+ : _messageGrouper = new SyncMessageGrouper(stdinStream ?? stdin),
+ _stdoutStream = stdoutStream ?? stdout;
+
+ @override
+ WorkRequest readRequest() {
+ var buffer = _messageGrouper.next;
+ if (buffer == null) return null;
+
+ return new WorkRequest.fromBuffer(buffer);
+ }
+
+ @override
+ void writeResponse(WorkResponse response) {
+ _stdoutStream.add(protoToDelimitedBuffer(response));
+ }
+}
diff --git a/pub/bazel_worker/lib/src/worker_loop.dart b/pub/bazel_worker/lib/src/worker/worker_loop.dart
similarity index 94%
rename from pub/bazel_worker/lib/src/worker_loop.dart
rename to pub/bazel_worker/lib/src/worker/worker_loop.dart
index 8eb7008..2e4a023 100644
--- a/pub/bazel_worker/lib/src/worker_loop.dart
+++ b/pub/bazel_worker/lib/src/worker/worker_loop.dart
@@ -2,7 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-import 'worker_protocol.pb.dart';
+import '../worker_protocol.pb.dart';
/// Interface for a [WorkerLoop].
///
diff --git a/pub/bazel_worker/lib/src/worker_connection.dart b/pub/bazel_worker/lib/src/worker_connection.dart
deleted file mode 100644
index c39bc3f..0000000
--- a/pub/bazel_worker/lib/src/worker_connection.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'worker_protocol.pb.dart';
-
-/// Interface for a [WorkerConnection].
-///
-/// Use [SyncWorkerConnection] or [AsyncWorkerConnection] implementations.
-abstract class WorkerConnection {
- /// Read a [WorkRequest]. Returns either [Future<WorkRequest>] or
- /// [WorkRequest].
- dynamic readRequest();
-
- /// Writes a [WorkResponse].
- void writeResponse(WorkResponse response);
-}
diff --git a/pub/bazel_worker/pubspec.yaml b/pub/bazel_worker/pubspec.yaml
index e6ca01b..9f88507 100644
--- a/pub/bazel_worker/pubspec.yaml
+++ b/pub/bazel_worker/pubspec.yaml
@@ -1,11 +1,11 @@
name: bazel_worker
-version: 0.1.3
+version: 0.1.4
description: Tools for creating a bazel persistent worker.
author: Dart Team <misc@dartlang.org>
homepage: https://github.com/dart-lang/bazel_worker
environment:
- sdk: '>=1.8.0 <2.0.0'
+ sdk: '>=1.22.1 <2.0.0'
dependencies:
async: ^1.9.0
diff --git a/pub/bazel_worker/tool/travis.sh b/pub/bazel_worker/tool/travis.sh
old mode 100644
new mode 100755
index 6b5e340..a011957
--- a/pub/bazel_worker/tool/travis.sh
+++ b/pub/bazel_worker/tool/travis.sh
@@ -10,7 +10,15 @@
# Verify that the libraries are error free.
dartanalyzer --fatal-warnings \
lib/bazel_worker.dart \
+ lib/driver.dart \
+ lib/testing.dart \
test/test_all.dart
# Run the tests.
pub run test
+
+pushd e2e_test
+pub get
+dartanalyzer --fatal-warnings test/e2e_test.dart
+pub run test
+popd
diff --git a/pub/browser/BUILD.gn b/pub/browser/BUILD.gn
new file mode 100644
index 0000000..b0e8833
--- /dev/null
+++ b/pub/browser/BUILD.gn
@@ -0,0 +1,14 @@
+# This file is generated by importer.py for browser-0.10.0+2
+
+import("//build/dart/dart_package.gni")
+
+dart_package("browser") {
+ package_name = "browser"
+
+ source_dir = "lib"
+
+ disable_analysis = true
+
+ deps = [
+ ]
+}
diff --git a/pub/browser/LICENSE b/pub/browser/LICENSE
new file mode 100644
index 0000000..5c60afe
--- /dev/null
+++ b/pub/browser/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2014, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pub/browser/README.md b/pub/browser/README.md
new file mode 100644
index 0000000..fa96851
--- /dev/null
+++ b/pub/browser/README.md
@@ -0,0 +1,39 @@
+This package contains dart.js, and previously contained interop.js
+
+dart.js
+=======
+
+The dart.js file is used in Dart browser apps to check for native Dart support
+and either (a) bootstrap Dartium or (b) load compiled JS instead. Previously,
+we've recommended that you add a script tag pointing the version of dart.js in
+our repository. This doesn't work offline and also results in slower startup
+(see [dartbug.com/6723](http://dartbug.com/6723)).
+
+Instead, we now recommend that you install dart.js via the following steps:
+
+1. Add the following to your pubspec.yaml:
+ dependencies:
+ browser: any
+
+2. Run pub install.
+
+3. Use a relative script tag in your html to the installed version:
+
+ `<script src="packages/browser/dart.js"></script>`
+
+If you do not wish to use pub, you may host a copy of this file locally instead.
+In this case, you will need to update it yourself as necessary. We reserve the
+right to move the old file in the repository, so we no longer recommend linking
+to it directly.
+
+interop.js
+==========
+
+This script was required for dart:js interop to work, but it is no longer
+needed. The functionality is now supported by dart:js directly.
+
+If you previously had a script such as this, please remove it:
+
+```html
+<script src="packages/browser/interop.js"></script>
+```
diff --git a/pub/browser/pubspec.yaml b/pub/browser/pubspec.yaml
new file mode 100644
index 0000000..3bf88f1
--- /dev/null
+++ b/pub/browser/pubspec.yaml
@@ -0,0 +1,8 @@
+name: browser
+version: 0.10.0+2
+author: "Dart Team <misc@dartlang.org>"
+homepage: http://www.dartlang.org
+description: >
+ The bootstrap dart.js script for Dart apps running in the browser.
+environment:
+ sdk: ">=1.3.0-dev.4.1 <2.0.0"
diff --git a/pub/charted/.gitignore b/pub/charted/.gitignore
new file mode 100644
index 0000000..9134752
--- /dev/null
+++ b/pub/charted/.gitignore
@@ -0,0 +1,6 @@
+build/
+*.swp
+packages
+.pub
+pubspec.lock
+.idea
diff --git a/pub/charted/BUILD.gn b/pub/charted/BUILD.gn
new file mode 100644
index 0000000..688ef12
--- /dev/null
+++ b/pub/charted/BUILD.gn
@@ -0,0 +1,19 @@
+# This file is generated by importer.py for charted-0.0.10
+
+import("//build/dart/dart_package.gni")
+
+dart_package("charted") {
+ package_name = "charted"
+
+ source_dir = "lib"
+
+ disable_analysis = true
+
+ deps = [
+ "//third_party/dart-pkg/pub/intl",
+ "//third_party/dart-pkg/pub/observe",
+ "//third_party/dart-pkg/pub/csslib",
+ "//third_party/dart-pkg/pub/logging",
+ "//third_party/dart-pkg/pub/browser",
+ ]
+}
diff --git a/pub/charted/LICENSE b/pub/charted/LICENSE
new file mode 100644
index 0000000..4b585f7
--- /dev/null
+++ b/pub/charted/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2014, Michael Bostock and Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived from this
+ software without specific prior written permission
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pub/charted/README.md b/pub/charted/README.md
new file mode 100644
index 0000000..5493d50
--- /dev/null
+++ b/pub/charted/README.md
@@ -0,0 +1,8 @@
+Visualization toolkit for Dart
+==============================
+[![Build Status](https://drone.io/github.com/google/charted/status.png)](https://drone.io/github.com/google/charted/latest)
+
+Charted provides
+* A selection API similar to D3.js
+* Visualization utilities ported from D3.js
+* An easy-to-use API to create charts including an SVG implementation based on the Selection API.
diff --git a/pub/charted/lib/charted.dart b/pub/charted/lib/charted.dart
new file mode 100644
index 0000000..c706efb
--- /dev/null
+++ b/pub/charted/lib/charted.dart
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+/**
+ * A library to create visualizations - implementation/port of D3.js in Dart.
+ */
+library charted;
+
+export 'charts/charts.dart';
+export 'core/core.dart';
+export 'event/event.dart';
+export 'interpolators/interpolators.dart';
+export 'layout/layout.dart';
+export 'locale/locale.dart';
+export 'scale/scale.dart';
+export 'selection/selection.dart';
+export 'transition/transition.dart';
+export 'svg/svg.dart';
diff --git a/pub/charted/lib/charts/behaviors/axis_marker.dart b/pub/charted/lib/charts/behaviors/axis_marker.dart
new file mode 100644
index 0000000..0ea0df6
--- /dev/null
+++ b/pub/charted/lib/charts/behaviors/axis_marker.dart
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class AxisMarker implements ChartBehavior {
+ ChartArea _area;
+ Rect _rect;
+
+ Element _markerX;
+ Element _markerY;
+
+ bool _showMarkerX = true;
+ bool _showMarkerY = true;
+ bool _showing;
+
+ Element _lower;
+ Element _upper;
+
+ StreamSubscription _mouseMoveSubscription;
+ StreamSubscription _mouseInSubscription;
+ StreamSubscription _mouseOutSubscription;
+
+ void init(ChartArea area, Element upper, Element lower) {
+ _area = area;
+ _lower = lower;
+ _upper = upper;
+
+ if (_area.dimensionAxesCount != 0) {
+ _mouseInSubscription = _area.onMouseOver.listen(_show);
+ _mouseOutSubscription = _area.onMouseOut.listen(_hide);
+ }
+ }
+
+ void dispose() {
+ if (_mouseInSubscription != null) _mouseInSubscription.cancel();
+ if (_mouseOutSubscription != null) _mouseOutSubscription.cancel();
+ if (_mouseMoveSubscription != null) _mouseOutSubscription.cancel();
+ if (_markerX != null) _markerX.remove();
+ if (_markerY != null) _markerY.remove();
+ }
+
+ void _show(ChartEvent e) {
+ if (_mouseMoveSubscription != null) return;
+ _create();
+ _visibility(true);
+ _mouseMoveSubscription = _area.onMouseMove.listen(_update);
+ }
+
+ void _hide(ChartEvent e) {
+ if (_showing != true) return;
+ _visibility(false);
+ _mouseMoveSubscription.cancel();
+ _mouseMoveSubscription = null;
+ }
+
+ void _visibility(bool show) {
+ if (_showing == show) return;
+ var value = show ? 'visible' : 'hidden';
+ if (_markerX != null) {
+ _markerX.style.visibility = value;
+ }
+ if (_markerY != null) {
+ _markerY.style.visibility = value;
+ }
+ }
+
+ bool _isRenderArea(ChartEvent e) =>
+ _rect != null && _rect.contains(e.chartX, e.chartY);
+
+ void _create() {
+ if (_rect == null) {
+ var renderArea = _area.layout.renderArea;
+ _rect = new Rect(
+ renderArea.x, renderArea.y, renderArea.width, renderArea.height);
+ }
+ if (_showMarkerX && _markerX == null) {
+ _markerX = new LineElement();
+ _markerX.attributes
+ ..['x1'] = '0'
+ ..['y1'] = _rect.y.toString()
+ ..['x2'] = '0'
+ ..['y2'] = (_rect.y + _rect.height).toString()
+ ..['class'] = 'axis-marker axis-marker-x';
+ _lower.append(_markerX);
+ }
+ if (_showMarkerY && _markerY == null) {
+ _markerY = new LineElement();
+ _markerY.attributes
+ ..['x1'] = _rect.x.toString()
+ ..['y1'] = '0'
+ ..['x2'] = (_rect.x + _rect.width).toString()
+ ..['y2'] = '0'
+ ..['class'] = 'axis-marker axis-marker-y';
+ _lower.append(_markerY);
+ }
+ _visibility(false);
+ }
+
+ void _update(ChartEvent e) {
+ if (!_isRenderArea(e)) {
+ _visibility(false);
+ } else {
+ _visibility(true);
+ window.requestAnimationFrame((_) {
+ if (_showMarkerX) {
+ _markerX.attributes['transform'] = 'translate(${e.chartX},0)';
+ }
+ if (_showMarkerY) {
+ _markerY.attributes['transform'] = 'translate(0,${e.chartY})';
+ }
+ });
+ }
+ }
+}
diff --git a/pub/charted/lib/charts/behaviors/chart_tooltip.dart b/pub/charted/lib/charts/behaviors/chart_tooltip.dart
new file mode 100644
index 0000000..bb2c2b7
--- /dev/null
+++ b/pub/charted/lib/charts/behaviors/chart_tooltip.dart
@@ -0,0 +1,173 @@
+part of charted.charts;
+
+/**
+ * The ChartTooltip displays tooltip for the values being interacted with in the
+ * chart. It displays all the active values in the data row and use the value
+ * in the dimension as the title.
+ */
+class ChartTooltip implements ChartBehavior {
+ static const _TOOLTIP_OFFSET = 26;
+ final String orientation;
+ final bool showDimensionValue;
+ final bool showMeasureTotal;
+
+ ChartArea _area;
+ Selection _tooltipSelection;
+ SubscriptionsDisposer _disposer = new SubscriptionsDisposer();
+
+ /**
+ * Constructs the tooltip, display extra fields base on [config] and position
+ * the tooltip base on [orientation] specified in the constructor.
+ */
+ ChartTooltip({this.showDimensionValue: false,
+ this.showMeasureTotal: false, this.orientation: ORIENTATION_RIGHT});
+
+ /** Sets up listeners for triggering tooltip. */
+ void init(ChartArea area, Element upperRenderPane, Element lowerRenderPane) {
+ _area = area;
+ _disposer.addAll([
+ area.onValueMouseOver.listen(show),
+ area.onValueMouseOut.listen(hide)
+ ]);
+
+ // Tooltip requires host to be position: relative.
+ area.host.style.position = 'relative';
+
+ var _scope = new SelectionScope.element(_area.host);
+ _scope.append('div')..classed('tooltip');
+ _tooltipSelection = _scope.select('.tooltip');
+ }
+
+ void dispose() {
+ _disposer.dispose();
+ if (_tooltipSelection != null) _tooltipSelection.remove();
+ }
+
+ /**
+ * Displays the tooltip upon receiving a hover event on an element in the
+ * chart.
+ */
+ show(ChartEvent e) {
+ // Clear
+ _tooltipSelection.first.children.clear();
+
+ // Display dimension value if set in config.
+ if (showDimensionValue) {
+ var column = _area.config.dimensions.elementAt(0),
+ value =
+ _area.data.rows.elementAt(e.row).elementAt(column),
+ formatter = _getFormatterForColumn(column);
+
+ _tooltipSelection.append('div')
+ ..classed('tooltip-title')
+ ..text((formatter != null) ? formatter(value) : value.toString());
+ }
+
+ // Display sum of the values in active row if set in config.
+ if (showMeasureTotal) {
+ var formatter =
+ _getFormatterForColumn(e.series.measures.elementAt(0));
+ var total = 0;
+ for (var i = 0; i < e.series.measures.length; i++) {
+ total += _area.data.rows.elementAt(e.row).
+ elementAt(e.series.measures.elementAt(i));
+ }
+ _tooltipSelection.append('div')
+ ..classed('tooltip-total')
+ ..text((formatter != null) ? formatter(total) : total.toString());
+ }
+
+ // Create the tooltip items base on the number of measures in the series.
+ var items = _tooltipSelection.selectAll('.tooltip-item').
+ data(e.series.measures);
+ items.enter.append('div')
+ ..classed('tooltip-item')
+ ..classedWithCallback('active', (d, i, c) => (i == e.column));
+
+ // Display the label for the currently active series.
+ var tooltipItems = _tooltipSelection.selectAll('.tooltip-item');
+ tooltipItems.append('div')
+ ..classed('tooltip-item-label')
+ ..textWithCallback((d, i, c) => _area.data.columns.
+ elementAt(e.series.measures.elementAt(i)).label);
+
+ // Display the value of the currently active series
+ tooltipItems.append('div')
+ ..classed('tooltip-item-value')
+ ..styleWithCallback('color', (d, i, c) =>
+ _area.theme.getColorForKey(d))
+ ..textWithCallback((d, i, c) {
+ var formatter = _getFormatterForColumn(d),
+ value = _area.data.rows.elementAt(e.row).elementAt(d);
+ return (formatter != null) ? formatter(value) : value.toString();
+ });
+
+ math.Point position = computeTooltipPosition(
+ new math.Point(e.chartX + _ChartArea.MARGIN,
+ e.chartY + _ChartArea.MARGIN),
+ _tooltipSelection.first.getBoundingClientRect());
+
+ // Set position of the tooltip and display it.
+ _tooltipSelection
+ ..style('left', '${position.x}px')
+ ..style('top', '${position.y}px')
+ ..style('opacity', '1');
+ }
+
+ /** Computes the ideal tooltip position based on orientation. */
+ math.Point computeTooltipPosition(math.Point coord,
+ math.Rectangle rect) {
+ var x, y;
+ if (orientation == ORIENTATION_TOP) {
+ x = coord.x - rect.width / 2;
+ y = coord.y - rect.height - _TOOLTIP_OFFSET;
+ } else if (orientation == ORIENTATION_RIGHT) {
+ x = coord.x + _TOOLTIP_OFFSET;
+ y = coord.y - rect.height / 2;
+ } else if (orientation == ORIENTATION_BOTTOM) {
+ x = coord.x - rect.width / 2;
+ y = coord.y + _TOOLTIP_OFFSET;
+ } else { // left
+ x = coord.x - rect.width - _TOOLTIP_OFFSET;
+ y = coord.y - rect.height / 2;
+ }
+
+ return boundTooltipPosition(
+ new math.Rectangle(x, y, rect.width, rect.height));
+ }
+
+ /** Positions the tooltip to be inside of the window boundary. */
+ math.Point boundTooltipPosition(math.Rectangle rect) {
+ var hostRect = _area.host.getBoundingClientRect();
+ var windowWidth = window.innerWidth;
+ var windowHeight = window.innerHeight;
+
+ var top = rect.top;
+ var left = rect.left;
+
+ // Checks top and bottom.
+ if (rect.top + hostRect.top < 0) {
+ top = -hostRect.top;
+ } else if (rect.top + rect.height + hostRect.top > windowHeight) {
+ top = windowHeight - rect.height - hostRect.top;
+ }
+
+ // Checks left and right.
+ if (rect.left < 0) {
+ left = -hostRect.left;
+ } else if (rect.left + rect.width + hostRect.left > windowWidth) {
+ left = windowWidth - rect.width - hostRect.left;
+ }
+
+ return new math.Point(left, top);
+ }
+
+ FormatFunction _getFormatterForColumn(int column) =>
+ _area.data.columns.elementAt(column).formatter;
+
+ hide(ChartEvent e) {
+ if (_tooltipSelection == null) return;
+ _tooltipSelection.style('opacity', '0');
+ }
+}
+
diff --git a/pub/charted/lib/charts/chart_area.dart b/pub/charted/lib/charts/chart_area.dart
new file mode 100644
index 0000000..8d3939f
--- /dev/null
+++ b/pub/charted/lib/charts/chart_area.dart
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Given a DOM element, [ChartArea] takes care of rendering the axis and
+ * passing relevant parameters to chart renderers that draw the actual
+ * data visualizations.
+ */
+abstract class ChartArea implements ChartBehaviorSource {
+ /**
+ * Data used by the chart. Chart isn't updated till the next call to
+ * draw function if [autoUpdate] is set to false.
+ *
+ * Setting new value to [data] will update the chart.
+ */
+ ChartData data;
+
+ /**
+ * Configuration for this chart. ChartBase subscribes to changes on
+ * [config] and calls draw upon any changes.
+ *
+ * Refer to [ChartConfig] for further documentation about which changes
+ * are added to the stream, which in turn trigger an update on the chart.
+ */
+ ChartConfig config;
+
+ /**
+ * Theme for this chart. Any changes to [theme] are not applied to the chart
+ * until it is redrawn. Changes can be forced by calling [draw] function.
+ */
+ ChartTheme theme;
+
+ /**
+ * When set to true, the chart subscribes to changes on data and updates the
+ * chart when [data] or [config] changes. Defaults to false.
+ */
+ bool autoUpdate;
+
+ /**
+ * Number of dimension axes that this area contains.
+ * Examples:
+ * - A bar-chart has one dimension axis (typically the 'x' axis)
+ * - A bubble-chart has two dimension axis (both 'x' and 'y')
+ * - A pie-chart does not have any axis
+ *
+ * Currently, the only valid values are 0, 1 and 2.
+ */
+ int dimensionAxesCount;
+
+ /**
+ * Geometry of components in this [ChartArea]
+ */
+ ChartAreaLayout get layout;
+
+ /**
+ * Scales used to render the measure axis of the given [ChartSeries]
+ */
+ Iterable<Scale> measureScales(ChartSeries s);
+
+ /**
+ * Scales used to render the dimension axes
+ */
+ Iterable<Scale> get dimensionScales;
+
+ /**
+ * Host of the ChartArea
+ */
+ Element get host;
+
+ /**
+ * Draw the chart with current data and configuration.
+ */
+ void draw();
+
+ /*
+ * Force destroy the ChartArea.
+ * - Clear references to all passed objects and subscriptions.
+ * - Call dispose on all renderers and behaviors.
+ */
+ void dispose();
+
+ /**
+ * Factory method to create an instance of the default implementation
+ * - [host] is the hosting element for the chart. The default
+ * implementation uses a HTML Element that has [Element.clientHeight]
+ * and [Element.clientWidth] defined.
+ * - [data] is an implementation of [ChartData] that is rendered.
+ * - [config] is an implementation of [ChartData]
+ * - [autoUpdate] indicates if the charts must be updated upon changes
+ * to data and config. If set to false, the chart isn't updated
+ * until [draw] is called.
+ * - [dimensionAxesCount] indicates the number of dimension axis
+ * displayed in the chart - currently, only 0, 1 and 2 are supported.
+ */
+ factory ChartArea(host, ChartData data, ChartConfig config,
+ {bool autoUpdate: false, int dimensionAxesCount: 1}) =>
+ new _ChartArea(host, data, config, autoUpdate, dimensionAxesCount);
+}
+
+/**
+ * Class representing geometry of the [ChartArea] and various components
+ * that are created by the ChartArea.
+ */
+abstract class ChartAreaLayout {
+ /** Sizes of the axes by orientation */
+ UnmodifiableMapView<String, Rect> get axes;
+
+ /** Size of render area */
+ Rect get renderArea => new Rect();
+
+ /** Size of chart area */
+ Rect get chartArea => new Rect();
+}
diff --git a/pub/charted/lib/charts/chart_config.dart b/pub/charted/lib/charts/chart_config.dart
new file mode 100644
index 0000000..42f7f2a
--- /dev/null
+++ b/pub/charted/lib/charts/chart_config.dart
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Configuration of the chart.
+ */
+abstract class ChartConfig {
+ /**
+ * List of series to visualize on this chart.
+ *
+ * If the implementation is observable, setting a new list must broadcast
+ * a change. Additionally, if [series] is set to an [ObservableList],
+ * changes to the list are broadcast too.
+ */
+ Iterable<ChartSeries> series;
+
+ /**
+ * List of columns that form the dimensions on the chart.
+ *
+ * If the implementation is observable, setting a new list must broadcast
+ * a change. Additionally, if [dimensions] is set to an [ObservableList],
+ * changes to the list are broadcast too.
+ */
+ Iterable<int> dimensions;
+
+ /** Instance of [ChartLegend] implementation used to render legend */
+ ChartLegend legend;
+
+ /** Recommended minimum size for the chart */
+ Rect minimumSize;
+
+ /** Indicates if the chart has primary dimension on the left axis */
+ bool leftAxisIsPrimary = false;
+
+ /** Registers axis configuration for the axis represented by [id]. */
+ void registerMeasureAxis(String id, ChartAxisConfig axis);
+
+ /** Return the user-set axis configuration for [id] */
+ ChartAxisConfig getMeasureAxis(String id);
+
+ /** Register axis configuration of the axis used for dimension [column]. */
+ void registerDimensionAxis(int column, ChartAxisConfig axis);
+
+ /**
+ * Return the user set axis configuration for [column]. If a custom scale
+ * was not set, returns null.
+ */
+ ChartAxisConfig getDimensionAxis(int column);
+
+ /**
+ * List of measure axes ids that are displayed. If not specified, the first
+ * two measure axes are displayed. If the list is empty, none of the
+ * measure axes are displayed.
+ */
+ Iterable<String> displayedMeasureAxes;
+
+ /**
+ * Indicates if the dimension axes should be drawn on this chart. Unless set
+ * to "false", the axes are rendered.
+ */
+ bool renderDimensionAxes;
+
+ /** Factory method to create an instance of the default implementation */
+ factory ChartConfig(Iterable<ChartSeries> series, Iterable<int> dimensions)
+ => new _ChartConfig(series, dimensions);
+}
+
+/**
+ * Implementation of [ChangeRecord] that is used to notify changes to
+ * [ChartConfig]. Currently, changes to list of dimensions and list of series
+ * are monitored.
+ */
+class ChartConfigChangeRecord implements ChangeRecord {
+ const ChartConfigChangeRecord();
+}
+
+/*
+ * Configuration for an axis
+ */
+class ChartAxisConfig {
+ /** Title for the axis */
+ String title;
+
+ /** Scale to be used with the axis */
+ Scale scale;
+
+ /**
+ * For a quantitative scale, values at which ticks should be displayed.
+ * When not specified, the ticks are intepolated evenly over the output
+ * range.
+ */
+ Iterable tickValues;
+}
+
diff --git a/pub/charted/lib/charts/chart_data.dart b/pub/charted/lib/charts/chart_data.dart
new file mode 100644
index 0000000..3b91021
--- /dev/null
+++ b/pub/charted/lib/charts/chart_data.dart
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Interface to be implemented by data providers to give tabular access to
+ * data for chart renderers.
+ */
+abstract class ChartData {
+ /**
+ * Create a new instance of [ChartData]'s internal implementation
+ */
+ factory ChartData(Iterable<ChartColumnSpec> columns, Iterable<Iterable> rows)
+ => new _ChartData(columns, rows);
+
+ /** Read-only access to column specs */
+ Iterable<ChartColumnSpec> get columns;
+
+ /** Read-only access to rows */
+ Iterable<Iterable> get rows;
+
+ @override
+ String toString();
+}
+
+/**
+ * Interface implemented by [ChartData] transformers.
+ * Examples:
+ * AggregationTranformer to aggregations rows/columns
+ * FilterTransformer to filter data
+ * TransposeTransformer to convert rows to columns and vice-versa
+ */
+abstract class ChartDataTransform {
+ /**
+ * Create a new instance of [ChartData] by selecting a subset
+ * of rows and columns from the current one
+ */
+ ChartData transform(ChartData source);
+}
+
+/**
+ * Implementation of [ChangeRecord], that is used to notify when rows get added
+ * or removed to ChartData
+ */
+class ChartRowChangeRecord implements ChangeRecord {
+ /**
+ * Changes to the rows - contains all updates to rows since last notification.
+ */
+ final List<ListChangeRecord> changes;
+
+ const ChartRowChangeRecord(this.changes);
+}
+
+/**
+ * Implementation of [ChangeRecord], that is used to notify changes to
+ * values in [ChartData].
+ */
+class ChartValueChangeRecord implements ChangeRecord {
+ /**
+ * Row that changes.
+ */
+ final int row;
+
+ /**
+ * List of changes to data on the row - includes all updates since the
+ * last change notification.
+ */
+ final List<ListChangeRecord> changes;
+
+ const ChartValueChangeRecord(this.row, this.changes);
+}
+
+/**
+ * Meta information for each column in ChartData
+ */
+class ChartColumnSpec {
+ static const String TYPE_BOOLEAN = 'boolean';
+ static const String TYPE_DATE = 'date';
+ static const String TYPE_NUMBER = 'number';
+ static const String TYPE_STRING = 'string';
+ static const String TYPE_TIMESTAMP = 'timestamp';
+
+ static const List ORDINAL_SCALES = const [ TYPE_STRING ];
+ static const List LINEAR_SCALES = const [ TYPE_NUMBER ];
+ static const List TIME_SCALES = const [ TYPE_DATE, TYPE_TIMESTAMP ];
+
+ /** Formatter for values that belong to this column */
+ final FormatFunction formatter;
+
+ /**
+ * Label for the column. Used in legend, tooltips etc;
+ * When not specified, defaults to empty string.
+ */
+ final String label;
+
+ /**
+ * Type of data in this column. Used for interpolations, computing
+ * scales and ranges. When not specified, it is assumed to be "number"
+ * for measures and "string" for dimensions.
+ */
+ final String type;
+
+ /**
+ * Indicates if this column requires an ordinal scale. Ordinal scales
+ * directly map the input value to one output value (i.e they do not
+ * use interpolations to compute the values. Eg: City names)
+ *
+ * If not specified, an ordinal scale is used for string columns and
+ * interpolated scales are used for others.
+ */
+ final bool useOrdinalScale;
+
+ /**
+ * Initialize axis scale according to [ChartColumnSpec] type.
+ * This logic is extracted from [ChartArea] implementation for conveniently
+ * adding more scale types.
+ */
+ Scale createDefaultScale() {
+ if (useOrdinalScale == true) return new OrdinalScale();
+ if (LINEAR_SCALES.contains(type)) return new LinearScale();
+ if (TIME_SCALES.contains(type)) return new TimeScale();
+ return null;
+ }
+
+ ChartColumnSpec({this.label, String type : TYPE_NUMBER,
+ this.formatter, bool useOrdinalScale})
+ : useOrdinalScale = useOrdinalScale == true ||
+ useOrdinalScale == null && ORDINAL_SCALES.contains(type),
+ type = type;
+}
diff --git a/pub/charted/lib/charts/chart_data_waterfall.dart b/pub/charted/lib/charts/chart_data_waterfall.dart
new file mode 100644
index 0000000..bb5fab6
--- /dev/null
+++ b/pub/charted/lib/charts/chart_data_waterfall.dart
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Interface to be implemented by data providers to give tabular access to
+ * data for waterfall chart renderer.
+ */
+abstract class WaterfallChartData extends ChartData {
+ /**
+ * Create a new instance of [ChartData]'s internal implementation
+ */
+ factory WaterfallChartData(Iterable<ChartColumnSpec> columns,
+ Iterable<Iterable> rows, [Iterable<int> baseRows]) =>
+ new _WaterfallChartData(columns, rows, baseRows);
+
+ /**
+ * Set of row indices that are drawn as base (no shifting on y-axis).
+ */
+ Iterable<int> baseRows;
+}
diff --git a/pub/charted/lib/charts/chart_events.dart b/pub/charted/lib/charts/chart_events.dart
new file mode 100644
index 0000000..a715606
--- /dev/null
+++ b/pub/charted/lib/charts/chart_events.dart
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+abstract class ChartBehaviorSource {
+ /**
+ * Stream of events that notify when a mouse button was pressed anywhere
+ * on the [ChartArea]
+ */
+ Stream<ChartEvent> get onMouseUp;
+
+ /**
+ * Stream of events that notify when an already pressed mouse button is
+ * released on the [ChartArea]
+ */
+ Stream<ChartEvent> get onMouseDown;
+
+ /**
+ * Stream of events that notify when mouse pointer enters [ChartArea]
+ */
+ Stream<ChartEvent> get onMouseOver;
+
+ /**
+ * Stream of events that notify when mouse pointer exits [ChartArea]
+ */
+ Stream<ChartEvent> get onMouseOut;
+
+ /**
+ * Stream of events that notify when mouse is moved on [ChartArea]
+ */
+ Stream<ChartEvent> get onMouseMove;
+
+ /**
+ * Stream of events that notify when a rendered value is clicked.
+ */
+ Stream<ChartEvent> get onValueClick;
+
+ /**
+ * Stream of events that notify when user moves mouse over a rendered value
+ */
+ Stream<ChartEvent> get onValueMouseOver;
+
+ /**
+ * Stream of events that notify when user moves mouse out of rendered value
+ */
+ Stream<ChartEvent> get onValueMouseOut;
+
+ /**
+ * A pane that is rendered below all the chart elements - for use with
+ * behaviors that add elements to chart.
+ */
+ Element get lowerBehaviorPane;
+
+ /**
+ * A pane that is rendered above all the chart elements - for use with
+ * behaviors that add elements to chart.
+ */
+ Element get upperBehaviorPane;
+
+ /**
+ * Add a behavior of ChartArea
+ */
+ void addChartBehavior(ChartBehavior behavior);
+
+ /**
+ * Remove a behavior of ChartArea
+ */
+ void removeChartBehavior(ChartBehavior behavior);
+}
+
+/**
+ * Class representing an event emitted by ChartEventSource
+ */
+abstract class ChartEvent {
+ /** DOM source event that caused this event */
+ Event get source;
+
+ /** ChartSeries if any on which this event occurred */
+ ChartSeries get series;
+
+ /** Column in ChartData on which this event occurred */
+ int get column;
+
+ /** Row in ChartData on which this event occurred */
+ int get row;
+
+ /** Value from ChartData on which the event occurred */
+ num get value;
+
+ /** X position relative to the rendered chart */
+ num get chartX;
+
+ /** Y position relative to the rendered chart */
+ num get chartY;
+}
+
+/**
+ * Interface implemented by chart behaviors.
+ * During initialization, the behaviors subscribe to any necessary events and
+ * handle them appropriately.
+ */
+abstract class ChartBehavior {
+ /**
+ * Called while ChartArea is being initialized.
+ * - [area] is the ChartArea on which this behavior is installed
+ * - [upperRenderPane] is an Element that is rendered on top of the
+ * chart. Behaviors can use it to draw any visualization in response
+ * to user actions.
+ * - [lowerRenderPane] is an Element that is rendered below the chart.
+ */
+ void init(ChartArea area, Element upperRenderPane, Element lowerRenderPane);
+
+ /**
+ * Clears all DOM created by this behavior, unsubscribes to event listeners
+ * and clears any state.
+ */
+ void dispose();
+}
diff --git a/pub/charted/lib/charts/chart_legend.dart b/pub/charted/lib/charts/chart_legend.dart
new file mode 100644
index 0000000..177d68c
--- /dev/null
+++ b/pub/charted/lib/charts/chart_legend.dart
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Interface that is implemented by classes that support
+ * displaying legend.
+ */
+abstract class ChartLegend {
+
+ /** Title of the legend, dynamically updates the legend title when set. */
+ String title;
+
+ /**
+ * Called by [ChartArea] to notify changes to legend.
+ */
+ update(Iterable<ChartLegendItem> legend, ChartArea chart);
+
+ /*
+ * Factory to create the default implementation of legend.
+ */
+ factory ChartLegend(Element host, {maxItems: 0, title: ''}) =>
+ new _ChartLegend(host, maxItems, title);
+}
+
+/**
+ * Class representing an item in the legend.
+ */
+class ChartLegendItem {
+ /** Index of the column in [ChartData] */
+ int column;
+
+ /** HTML color used for this column in the chart */
+ String color;
+
+ /** The label of the Legend Item. */
+ String label;
+
+ /** List of series that this column is part of */
+ Iterable<ChartSeries> series;
+
+ /** Utility constructors */
+ ChartLegendItem({this.column, this.color, this.label, this.series});
+}
diff --git a/pub/charted/lib/charts/chart_renderer.dart b/pub/charted/lib/charts/chart_renderer.dart
new file mode 100644
index 0000000..9f1af55
--- /dev/null
+++ b/pub/charted/lib/charts/chart_renderer.dart
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Renders the chart on a compatible [ChartArea].
+ */
+abstract class ChartRenderer {
+ /**
+ * Returns extent of the series. This extent is used by [ChartArea] to
+ * set the output range of the corresponding scale/axis of the series.
+ *
+ * Extent has valid values only if [prepare] was already called.
+ */
+ Extent get extent;
+
+ /**
+ * Indicates if this renderer uses range "band" on any of the dimension
+ * axis. Band is space taken on the dimension axis (if more than a point).
+ *
+ * Examples:
+ * A bar chart takes up space (width of the bar) on the dimension axis.
+ * A line chart does not take any space
+ */
+ Iterable<int> get dimensionsUsingBand;
+
+ /**
+ * Hint for padding between two bands that the ChartArea could use. This
+ * getter is called only for renderers that have [dimensionsUsingBand]
+ * set to non-empty list.
+ */
+ double get bandInnerPadding;
+
+ /**
+ * Hint for padding before the first and after the last bands that the
+ * ChartArea could use. This getter is called only for renderers that have
+ * [dimensionsUsingBand] set to non-empty list.
+ */
+ double get bandOuterPadding;
+
+ /**
+ * Stream of events that indicate that user clicked on a displayed value
+ */
+ Stream<ChartEvent> get onValueMouseClick;
+
+ /**
+ * Stream of events that indicate that user moved the mouse pointer out
+ * over a displayed value
+ */
+ Stream<ChartEvent> get onValueMouseOver;
+
+ /**
+ * Stream of events that indicate that user moved the mouse pointer out
+ * of a displayed value
+ */
+ Stream<ChartEvent> get onValueMouseOut;
+
+ /**
+ * Prepare the chart for rendering.
+ * - [area] represents the [ChartArea] on which the chart is rendered.
+ * - [series] represents the [ChartSeries] that is rendered
+ */
+ bool prepare(ChartArea area, ChartSeries series);
+
+ /**
+ * Render series data on the passed [host].
+ * Draw will not be successful if [prepare] was not already called.
+ */
+ void draw(Element host);
+
+ /**
+ * Clears DOM created by this renderer and releases
+ * references to passed objects.
+ */
+ void dispose();
+}
diff --git a/pub/charted/lib/charts/chart_series.dart b/pub/charted/lib/charts/chart_series.dart
new file mode 100644
index 0000000..0d4a76f
--- /dev/null
+++ b/pub/charted/lib/charts/chart_series.dart
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * A [ChartSeries] represents one or more columns in ChartData that are
+ * rendered together.
+ *
+ * Examples:
+ * 1. For bar-chart or line-chart, a series consists of one column
+ * 2. For stacked chart or grouped bar chart, a series has more than columns
+ */
+class ChartSeries {
+ /** Name of the series */
+ final String name;
+
+ /**
+ * Optional Ids of measure axes.
+ *
+ * When specified renderers scale the column values against the ranges
+ * of the given axes. If an axis with a matching Id does not exist in
+ * [ChartArea] a new axis is created.
+ *
+ * When not specified, renderers may use [ChartArea.defaultMeasureAxis]
+ * where ever necessary. Refer to the implementation of [ChartRenderer] for
+ * more information on defaults and how the measure axes are used.
+ *
+ * If the implementation is [Observable] and [measureAxisIds] is set to an
+ * [ObservableList], changes to the list must be broadcasted.
+ */
+ Iterable<String> measureAxisIds;
+
+ /**
+ * List of columns in ChartData that are measures of this series.
+ *
+ * A series may include more than one measure if the renderer supports it.
+ * When there are more measures than what the renderer can handle, a renderer
+ * only renders the first "supported number" of columns. If the number of
+ * columns is less than the minimum that the renderer supports, the remaining
+ * measures are assumed to have zeros.
+ *
+ * If the implementation is [Observable] and [measures] is set to an
+ * [ObservableList], changes to the list must be broadcasted.
+ */
+ Iterable<int> measures;
+
+ /**
+ * Instance of the renderer used to render the series.
+ *
+ * [ChartArea] creates a renderer using [ChartRender.create] and uses it
+ * to compute range of the measure axis and to render the chart.
+ */
+ ChartRenderer renderer;
+
+ /**
+ * Factory function to create an instance of internal implementation of
+ * [ChartSeries].
+ */
+ factory ChartSeries(String name, Iterable<int> measures,
+ ChartRenderer renderer, { Iterable<String> measureAxisIds : null })
+ => new _ChartSeries(name, measures, renderer, measureAxisIds);
+}
+
+/**
+ * Implementation of [ChangeRecord] that is used to notify changes to
+ * [ChartSeries]. Currently, only changes to measures and measureAxisIds
+ * are supported.
+ */
+class ChartSeriesChangeRecord implements ChangeRecord {
+ /**
+ * Reference to series that changed
+ */
+ final ChartSeries series;
+
+ const ChartSeriesChangeRecord(this.series);
+}
\ No newline at end of file
diff --git a/pub/charted/lib/charts/chart_theme.dart b/pub/charted/lib/charts/chart_theme.dart
new file mode 100644
index 0000000..68cece9
--- /dev/null
+++ b/pub/charted/lib/charts/chart_theme.dart
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Theme used to render the chart area, specifically colors and axes.
+ *
+ * Typical implementations of ChartTheme also implement theme interfaces
+ * used by the renderers, tooltips, legends and any other behaviors.
+ */
+
+abstract class ChartTheme {
+ static ChartTheme current = new QuantumChartTheme();
+
+ /** Column/series when it is disabled, possibly because another is active */
+ static const int STATE_DISABLED = 0;
+
+ /** Column/Series that is normal */
+ static const int STATE_NORMAL = 1;
+
+ /** Column/series that is active, possibly by a click */
+ static const int STATE_ACTIVE = 2;
+
+ /**
+ * Color that can be used for key.
+ * For a given input key, the output is always the same.
+ */
+ String getColorForKey(key, [int state]);
+
+ /**
+ * Width of the separator between two chart elements.
+ * Used to separate pies in pie-chart, bars in grouped and stacked charts.
+ */
+ int get defaultSeparatorWidth => 1;
+
+ /**
+ * Stroke width used by all shapes.
+ * It also used when computing width of the by the renderers.
+ */
+ int get defaultStrokeWidth => 2;
+
+ /** Easing function for the transition */
+ EasingFn get transitionEasingType => Transition.defaultEasingType;
+
+ /** Easing mode for the transition */
+ EasingMode get transitionEasingMode => Transition.defaultEasingMode;
+
+ /** Total duration of the transision in milli-seconds */
+ int get transitionDuration => 250;
+
+ /** Theme passed to the measure axes */
+ ChartAxisTheme get measureAxisTheme;
+
+ /** Theme passed to the dimension axes */
+ ChartAxisTheme get dimensionAxisTheme;
+}
+
+abstract class ChartAxisTheme {
+ /**
+ * Treshold for tick length. Setting [axisTickSize] <= [FILL_RENDER_AREA]
+ * will make the axis span the entire height/width of the rendering area.
+ */
+ static const int FILL_RENDER_AREA = SMALL_INT_MIN;
+
+ /**
+ * Number of ticks displayed on the axis - only used when an axis is
+ * using a quantitative scale.
+ */
+ int get axisTickCount;
+
+ /**
+ * Size of ticks on the axis. When [measureTickSize] <= [FILL_RENDER_AREA],
+ * the painted tick will span complete height/width of the rendering area.
+ */
+ int get axisTickSize;
+
+ /** Space between axis and label for dimension axes */
+ int get axisTickPadding;
+
+ /**
+ * Space between the first tick and the measure axes.
+ * Only used on charts that don't have renderers that use "bands" of space
+ * on the dimension axes
+ *
+ * Represented as a percentage of space between two consecutive ticks. The
+ * space between two consecutive ticks is also known as the segment size.
+ */
+ double get axisOuterPadding;
+
+ /**
+ * Space between the two bands in the chart.
+ * Only used on charts that have renderers that use "bands" of space on the
+ * dimension axes.
+ *
+ * Represented as a percentage of space between two consecutive ticks. The
+ * space between two consecutive ticks is also known as the segment size.
+ */
+ double get axisBandInnerPadding;
+
+ /**
+ * Space between the first band and the measure axis.
+ * Only used on charts that have renderers that use "bands" of space on the
+ * dimension axes.
+ *
+ * Represented as a percentage of space between two consecutive ticks. The
+ * space between two consecutive ticks is also known as the segment size.
+ */
+ double get axisBandOuterPadding;
+
+ /** When set to true, the axes resize to fit the labels. */
+ bool get axisAutoResize => true;
+
+ /**
+ * Width of vertical axis when it is not resizing automatically. If
+ * [autoResizeAxis] is set to true, [verticalAxisWidth] will be used as the
+ * maximum width of the vertical axis.
+ *
+ * Height of vertical axis is automatically computed based on height of the
+ * visualization.
+ */
+ int get verticalAxisWidth => 200;
+
+ /**
+ * Height of horizontal axis when it is not resizing automatically. If
+ * [autoResizeAxis] is set to true [horizontalAxisHeight] is used as the
+ * maximum height of the horizontal axis.
+ *
+ * Width of horizontal axis is automatically computed based on width of the
+ * visualization.
+ */
+ int get horizontalAxisHeight => 200;
+}
diff --git a/pub/charted/lib/charts/charts.dart b/pub/charted/lib/charts/charts.dart
new file mode 100644
index 0000000..738a02c
--- /dev/null
+++ b/pub/charted/lib/charts/charts.dart
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+library charted.charts;
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:html' show Element, document, window, Event, MouseEvent;
+import 'dart:math' as math;
+import 'dart:svg' hide Rect;
+import 'dart:typed_data';
+
+import 'package:charted/core/core.dart';
+import 'package:charted/layout/layout.dart';
+import 'package:charted/locale/locale.dart';
+import 'package:charted/scale/scale.dart';
+import 'package:charted/selection/selection.dart';
+import 'package:charted/svg/svg.dart';
+import 'package:charted/transition/transition.dart';
+import 'package:collection/equality.dart';
+import 'package:logging/logging.dart';
+import 'package:observe/observe.dart';
+import 'package:charted/interpolators/interpolators.dart';
+
+part 'chart_area.dart';
+part 'chart_config.dart';
+part 'chart_data.dart';
+part 'chart_data_waterfall.dart';
+part 'chart_events.dart';
+part 'chart_legend.dart';
+part 'chart_renderer.dart';
+part 'chart_series.dart';
+part 'chart_theme.dart';
+
+part 'behaviors/axis_marker.dart';
+part 'behaviors/chart_tooltip.dart';
+
+part 'renderers/bar_chart_renderer.dart';
+part 'renderers/base_renderer.dart';
+part 'renderers/bubble_chart_renderer.dart';
+part 'renderers/line_chart_renderer.dart';
+part 'renderers/pie_chart_renderer.dart';
+part 'renderers/stackedbar_chart_renderer.dart';
+part 'renderers/waterfall_chart_renderer.dart';
+
+part 'src/chart_area_impl.dart';
+part 'src/chart_axis_impl.dart';
+part 'src/chart_config_impl.dart';
+part 'src/chart_data_impl.dart';
+part 'src/chart_data_waterfall_impl.dart';
+part 'src/chart_events_impl.dart';
+part 'src/chart_legend_impl.dart';
+part 'src/chart_series_impl.dart';
+
+part 'themes/quantum_theme.dart';
+
+part 'transformers/aggregation.dart';
+part 'transformers/aggregation_item.dart';
+part 'transformers/aggregation_transformer.dart';
+part 'transformers/filter_transformer.dart';
+part 'transformers/transpose_transformer.dart';
+
+final Logger logger = new Logger('charted.charts');
+
+class SubscriptionsDisposer {
+ List<StreamSubscription> _subscriptions = [];
+ Expando<StreamSubscription> _byObject = new Expando();
+
+ void add(StreamSubscription value, [Object handle]) {
+ if (handle != null) _byObject[handle] = value;
+ _subscriptions.add(value);
+ }
+
+ void addAll(List<StreamSubscription> values, [Object handle]) {
+ for (var subscription in values) {
+ add(subscription, handle);
+ }
+ }
+
+ void unsubscribe(Object handle) {
+ StreamSubscription s = _byObject[handle];
+ if (s != null) {
+ _subscriptions.remove(s);
+ s.cancel();
+ }
+ }
+
+ void dispose() {
+ _subscriptions.forEach((StreamSubscription val) {
+ if (val != null) val.cancel();
+ });
+ _subscriptions.clear();
+ }
+}
diff --git a/pub/charted/lib/charts/renderers/bar_chart_renderer.dart b/pub/charted/lib/charts/renderers/bar_chart_renderer.dart
new file mode 100644
index 0000000..76a611d
--- /dev/null
+++ b/pub/charted/lib/charts/renderers/bar_chart_renderer.dart
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class BarChartRenderer extends BaseRenderer {
+ final Iterable<int> dimensionsUsingBand = const[0];
+
+ /*
+ * Returns false if the number of dimension axes on the area is 0.
+ * Otherwise, the first dimension scale is used to render the chart.
+ */
+ @override
+ bool prepare(ChartArea area, ChartSeries series) {
+ _ensureAreaAndSeries(area, series);
+ return area.dimensionAxesCount != 0;
+ }
+
+ @override
+ void draw(Element element) {
+ _ensureReadyToDraw(element);
+
+ var measuresCount = series.measures.length,
+ measureScale = area.measureScales(series).first,
+ dimensionScale = area.dimensionScales.first;
+
+ var rows = new List()..addAll(area.data.rows.map((e) {
+ var row = [];
+ for (var measure in series.measures) {
+ row.add(e[measure]);
+ }
+ return row;
+ }));
+
+ var x = area.data.rows.map(
+ (row) => row.elementAt(area.config.dimensions.first)).toList();
+
+ var bars = new OrdinalScale()
+ ..domain = new Range(series.measures.length).toList()
+ ..rangeRoundBands([0, dimensionScale.rangeBand]);
+
+ var groups = root.selectAll('.row-group').data(rows);
+
+ groups.enter.append('g')
+ ..classed('row-group')
+ ..attrWithCallback('transform', (d, i, c) =>
+ 'translate(${dimensionScale.apply(x[i])}, 0)');
+ groups.exit.remove();
+
+ // TODO(psunkari): Try not to set an attribute with row index on the gorup.
+ groups.transition()
+ ..attrWithCallback('transform', (d, i, c) =>
+ 'translate(${dimensionScale.apply(x[i])}, 0)')
+ ..attrWithCallback('data-row', (d, i, e) => i)
+ ..duration(theme.transitionDuration);
+
+ int barWidth = bars.rangeBand -
+ theme.defaultSeparatorWidth - theme.defaultStrokeWidth;
+
+ var bar = groups.selectAll('.bar').dataWithCallback((d, i, c) => rows[i]);
+ var enter = bar.enter.append('rect')
+ ..classed('bar')
+ ..attr('y', rect.height)
+ ..attr('height', 0)
+ ..styleWithCallback('fill', (d, i, c) => colorForKey(i))
+ ..attrWithCallback(
+ 'x', (d, i, e) => bars.apply(i) + theme.defaultStrokeWidth)
+ ..attr('width', barWidth)
+ ..on('click', (d, i, e) => _event(mouseClickController, d, i, e))
+ ..on('mouseover', (d, i, e) => _event(mouseOverController, d, i, e))
+ ..on('mouseout', (d, i, e) => _event(mouseOutController, d, i, e));
+
+ bar.transition()
+ ..attrWithCallback(
+ 'x', (d, i, c) => bars.apply(i) + theme.defaultStrokeWidth)
+ ..styleWithCallback('fill', (d, i, c) => colorForKey(i))
+ ..attr('width', barWidth)
+ ..duration(theme.transitionDuration);
+
+ int delay = 0;
+ bar.transition()
+ ..attrWithCallback('y', (d, i, c) => measureScale.apply(d).round())
+ // height -1 so bar does not overlap x axis.
+ ..attrWithCallback('height', (d, i, c) {
+ var height = rect.height - measureScale.apply(d).round() - 1;
+ return (height < 0) ? 0 : height;
+ })
+ ..delayWithCallback((d, i, c) =>
+ delay += theme.transitionDuration ~/
+ (series.measures.length * rows.length));
+
+ if (theme.defaultStrokeWidth > 0) {
+ enter.attr('stroke-width', '${theme.defaultStrokeWidth}px');
+ enter.styleWithCallback('stroke', (d, i, c) => colorForKey(i));
+ bar.transition()
+ ..styleWithCallback('stroke', (d, i, c) => colorForKey(i));
+ }
+
+ bar.exit.remove();
+ }
+
+ @override
+ double get bandInnerPadding {
+ assert(series != null && area != null);
+ var measuresCount = series.measures.length;
+ return measuresCount > 2 ? 1 - (measuresCount / (measuresCount + 1)) :
+ area.theme.dimensionAxisTheme.axisBandInnerPadding;
+ }
+
+ @override
+ double get bandOuterPadding {
+ assert(series != null && area != null);
+ return area.theme.dimensionAxisTheme.axisBandOuterPadding;
+ }
+
+ void _event(StreamController controller, data, int index, Element e) {
+ if (controller == null) return;
+ var rowStr = e.parent.dataset['row'];
+ var row = rowStr != null ? int.parse(rowStr) : null;
+ controller.add(
+ new _ChartEvent(scope.event, area, series, row, index, data));
+ }
+}
diff --git a/pub/charted/lib/charts/renderers/base_renderer.dart b/pub/charted/lib/charts/renderers/base_renderer.dart
new file mode 100644
index 0000000..263c474
--- /dev/null
+++ b/pub/charted/lib/charts/renderers/base_renderer.dart
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class BaseRenderer implements ChartRenderer {
+ ChartArea area;
+ ChartSeries series;
+ ChartTheme theme;
+ Rect rect;
+
+ Element host;
+ Selection root;
+ SelectionScope scope;
+
+ StreamController<ChartEvent> mouseOverController;
+ StreamController<ChartEvent> mouseOutController;
+ StreamController<ChartEvent> mouseClickController;
+
+ void _ensureAreaAndSeries(ChartArea area, ChartSeries series) {
+ assert(area != null && series != null);
+ this.area = area;
+ this.series = series;
+ }
+
+ void _ensureReadyToDraw(Element element) {
+ assert(series != null && area != null);
+ assert(element != null && element is GElement);
+
+ if (scope == null) {
+ host = element;
+ scope = new SelectionScope.element(element);
+ root = scope.selectElements([host]);
+ }
+
+ theme = area.theme;
+ rect = area.layout.renderArea;
+ }
+
+ @override
+ void dispose() {
+ if (root == null) return;
+ root.selectAll('.row-group').remove();
+ }
+
+ @override
+ Extent get extent {
+ assert(series != null && area != null);
+ var rows = area.data.rows,
+ max = rows[0][series.measures.first],
+ min = max;
+
+ rows.forEach((row) {
+ series.measures.forEach((idx) {
+ if (row[idx] > max) max = row[idx];
+ if (row[idx] < min) min = row[idx];
+ });
+ });
+ return new Extent(min, max);
+ }
+
+ @override
+ Stream<ChartEvent> get onValueMouseOver {
+ if (mouseOverController == null) {
+ mouseOverController = new StreamController.broadcast(sync: true);
+ }
+ return mouseOverController.stream;
+ }
+
+ @override
+ Stream<ChartEvent> get onValueMouseOut {
+ if (mouseOutController == null) {
+ mouseOutController = new StreamController.broadcast(sync: true);
+ }
+ return mouseOutController.stream;
+ }
+
+ @override
+ Stream<ChartEvent> get onValueMouseClick {
+ if (mouseClickController == null) {
+ mouseClickController = new StreamController.broadcast(sync: true);
+ }
+ return mouseClickController.stream;
+ }
+
+ double get bandInnerPadding => 1.0;
+ double get bandOuterPadding => area.theme.dimensionAxisTheme.axisOuterPadding;
+
+ /** Get a color using the theme's ordinal scale of colors */
+ String colorForKey(i) =>
+ area.theme.getColorForKey(series.measures.elementAt(i));
+
+ /** List of measure values as rows containing only measure columns */
+ Iterable<Iterable> get asRowValues => [];
+
+ /** List of measure values as columns */
+ Iterable<Iterable> get asColumnValues => [];
+}
\ No newline at end of file
diff --git a/pub/charted/lib/charts/renderers/bubble_chart_renderer.dart b/pub/charted/lib/charts/renderers/bubble_chart_renderer.dart
new file mode 100644
index 0000000..85e6eef
--- /dev/null
+++ b/pub/charted/lib/charts/renderers/bubble_chart_renderer.dart
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class BubbleChartRenderer implements ChartRenderer {
+ final Iterable<int> dimensionsUsingBand = const[];
+
+ ChartArea area;
+ ChartSeries series;
+
+ final double maxBubbleRadius;
+
+ Element _host;
+ Selection _group;
+ SelectionScope _scope;
+
+ StreamController<ChartEvent> _mouseOverController;
+ StreamController<ChartEvent> _mouseOutController;
+ StreamController<ChartEvent> _mouseClickController;
+
+ BubbleChartRenderer([this.maxBubbleRadius = 20.0]);
+
+ /*
+ * BubbleChart needs two dimension axes.
+ */
+ @override
+ bool prepare(ChartArea area, ChartSeries series) {
+ assert(area != null && series != null);
+ if (area.dimensionAxesCount != 2) return false;
+ this.area = area;
+ this.series = series;
+ return true;
+ }
+
+ @override
+ void draw(Element element) {
+ assert(series != null && area != null);
+ assert(element != null && element is GElement);
+
+ if (_scope == null) {
+ _host = element;
+ _scope = new SelectionScope.element(element);
+ _group = _scope.selectElements([_host]);
+ }
+
+ var geometry = area.layout.renderArea,
+ measuresCount = series.measures.length,
+ bubbleRadiusScale = area.measureScales(series).first,
+ xDimensionScale = area.dimensionScales.first,
+ yDimensionScale = area.dimensionScales.last,
+ theme = area.theme,
+ bubbleRadiusFactor =
+ maxBubbleRadius / min([geometry.width, geometry.height]);
+
+ String color(i) => theme.getColorForKey(series.measures.elementAt(i));
+
+ // Measure values used to set size of the bubble.
+ var columns = [];
+ for (int m in series.measures) {
+ columns.add(new List.from(
+ area.data.rows.map((Iterable row) => row.elementAt(m))));
+ }
+
+ // Dimension values used to position the bubble.
+ var xDimensionIndex = area.config.dimensions.first,
+ yDimensionIndex = area.config.dimensions.last,
+ xDimensionVals = [],
+ yDimensionVals = [];
+ for (var row in area.data.rows) {
+ xDimensionVals.add(row.elementAt(xDimensionIndex));
+ yDimensionVals.add(row.elementAt(yDimensionIndex));
+ }
+
+ var group = _group.selectAll('.measure-group').data(columns);
+ group.enter.append('g')
+ ..classed('measure-group')
+ ..styleWithCallback('fill', (d, i, e) => color(i));
+ group.exit.remove();
+
+ var measures = group.selectAll('.bubble').dataWithCallback(
+ (d, i, e) => columns[i]);
+ var enter = measures.enter.append('circle')
+ ..classed('bubble')
+ ..attrWithCallback('transform',
+ (d, i, e) => 'translate('
+ '${xDimensionScale.apply(xDimensionVals[i])},'
+ '${yDimensionScale.apply(yDimensionVals[i])})')
+ ..attrWithCallback('r',
+ (d, i, e) => '${bubbleRadiusScale.apply(d) * bubbleRadiusFactor}');
+ measures.exit.remove();
+ }
+
+ @override
+ void dispose() {
+ if (_group == null) return;
+ _group.selectAll('.row-group').remove();
+ }
+
+ @override
+ double get bandInnerPadding => 1.0;
+
+ @override
+ double get bandOuterPadding => area.theme.dimensionAxisTheme.axisOuterPadding;
+
+ @override
+ Extent get extent {
+ assert(series != null && area != null);
+ var rows = area.data.rows,
+ max = rows[0][series.measures.first],
+ min = max;
+
+ rows.forEach((row) {
+ series.measures.forEach((idx) {
+ if (row[idx] > max) max = row[idx];
+ if (row[idx] < min) min = row[idx];
+ });
+ });
+ return new Extent(min, max);
+ }
+
+ void _event(StreamController controller, data, int index, Element e) {
+ if (controller == null) return;
+ var rowStr = e.parent.dataset['row'];
+ var row = rowStr != null ? int.parse(rowStr) : null;
+ controller.add(
+ new _ChartEvent(_scope.event, area, series, row, index, data));
+ }
+
+ @override
+ Stream<ChartEvent> get onValueMouseOver {
+ if (_mouseOverController == null) {
+ _mouseOverController = new StreamController.broadcast(sync: true);
+ }
+ return _mouseOverController.stream;
+ }
+
+ @override
+ Stream<ChartEvent> get onValueMouseOut {
+ if (_mouseOutController == null) {
+ _mouseOutController = new StreamController.broadcast(sync: true);
+ }
+ return _mouseOutController.stream;
+ }
+
+ @override
+ Stream<ChartEvent> get onValueMouseClick {
+ if (_mouseClickController == null) {
+ _mouseClickController = new StreamController.broadcast(sync: true);
+ }
+ return _mouseClickController.stream;
+ }
+}
diff --git a/pub/charted/lib/charts/renderers/line_chart_renderer.dart b/pub/charted/lib/charts/renderers/line_chart_renderer.dart
new file mode 100644
index 0000000..b651a0c
--- /dev/null
+++ b/pub/charted/lib/charts/renderers/line_chart_renderer.dart
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class LineChartRenderer extends BaseRenderer {
+ final Iterable<int> dimensionsUsingBand = const[];
+
+ /*
+ * Returns false if the number of dimension axes on the area is 0.
+ * Otherwise, the first dimension scale is used to render the chart.
+ */
+ @override
+ bool prepare(ChartArea area, ChartSeries series) {
+ _ensureAreaAndSeries(area, series);
+ return area.dimensionAxesCount != 0;
+ }
+
+ @override
+ void draw(Element element) {
+ _ensureReadyToDraw(element);
+
+ var measuresCount = series.measures.length,
+ measureScale = area.measureScales(series).first,
+ dimensionScale = area.dimensionScales.first;
+
+ // Create initial values for transitiion
+ var initialValues = series.measures.map((column) {
+ return area.data.rows.map((values) => 0).toList();
+ }).toList();
+
+ // Create lists of values in measure columns.
+ var lines = series.measures.map((column) {
+ return area.data.rows.map((values) => values[column]).toList();
+ }).toList();
+
+ // We only support one dimension axes, so we always use the
+ // first dimension.
+ var x = area.data.rows.map(
+ (row) => row.elementAt(area.config.dimensions.first)).toList();
+
+ var rangeBandOffset = dimensionScale.rangeBand / 2;
+ var _xAccessor = (d, i) => dimensionScale.apply(x[i]) + rangeBandOffset;
+ var _yAccessor = (d, i) => measureScale.apply(d);
+
+ var line = new SvgLine();
+ line.xAccessor = _xAccessor;
+ line.yAccessor = _yAccessor;
+
+ // Draw the lines.
+ // TODO (midoringo): Right now the default stroke-width is 2px, which is
+ // hard for user to hover over. Maybe add an larger, invisible capture area?
+ var svgLines = root.selectAll('.line').data(initialValues);
+ svgLines.enter.append('path')
+ ..classed('line', true)
+ ..attrWithCallback('d', (d, i, e) => line.path(d, i, e))
+ ..styleWithCallback('stroke', (d, i, e) => colorForKey(i))
+ ..on('mouseover', (d, i, e) {
+ // Thickens the line on hover and show the points.
+ root.selectAll('.line-point-${i}')..style('opacity', '1');
+ e.classes.add('active');
+ })
+ ..on('mouseout', (d, i, e) {
+ // Thins the line on mouse out and hide the points.
+ root.selectAll('.line-point-${i}')..style('opacity', '0');
+ e.classes.remove('active');
+ })
+ ..style('fill', 'none');
+
+ int delay = 0;
+ svgLines = root.selectAll('.line').data(lines);
+ svgLines.transition()
+ ..attrWithCallback('d', (d, i, e) => line.path(d, i, e))
+ ..styleWithCallback('stroke', (d, i, e) => colorForKey(i))
+ ..duration(theme.transitionDuration)
+ ..delayWithCallback((d, i, c) => delay += 50 ~/ series.measures.length);
+ svgLines.exit.remove();
+
+ // Draw the circle for each point in line for events.
+ for (var columnIndex = 0; columnIndex < lines.length; columnIndex++) {
+ root.selectAll('.line-point-${columnIndex}').remove();
+ var points = root.selectAll('point').data(lines[columnIndex]);
+ points.enter.append('circle')
+ ..classed('line-point line-point-${columnIndex}', true)
+ ..attr('r', 4)
+ ..attrWithCallback('data-row', (d, i, e) => i)
+ ..attrWithCallback('cx', (d, i, e) => _xAccessor(d, i))
+ ..attrWithCallback('cy', (d, i, e) => _yAccessor(d, i))
+ ..styleWithCallback('stroke', (d, i, e) => colorForKey(columnIndex))
+ ..styleWithCallback('fill', (d, i, e) => colorForKey(columnIndex))
+ ..style('opacity', '0')
+ ..on('click',
+ (d, i, e) => _event(mouseClickController, d, columnIndex, e))
+ ..on('mouseover', (d, i, e) {
+ e.style.opacity = '1';
+ _event(mouseOverController, d, columnIndex, e);
+ })
+ ..on('mouseout', (d, i, e) {
+ e.style.opacity = '0';
+ _event(mouseOutController, d, columnIndex, e);
+ });
+ }
+ }
+
+ @override
+ void dispose() {
+ if (root == null) return;
+ root.selectAll('.line').remove();
+ }
+
+ void _event(StreamController controller, data, int index, Element e) {
+ if (controller == null) return;
+ var rowStr = e.dataset['row'];
+ var row = rowStr != null ? int.parse(rowStr) : null;
+ controller.add(
+ new _ChartEvent(scope.event, area, series, row, index, data));
+ }
+}
diff --git a/pub/charted/lib/charts/renderers/pie_chart_renderer.dart b/pub/charted/lib/charts/renderers/pie_chart_renderer.dart
new file mode 100644
index 0000000..14525e2
--- /dev/null
+++ b/pub/charted/lib/charts/renderers/pie_chart_renderer.dart
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class PieChartRenderer extends BaseRenderer {
+ static const STATS_PERCENTAGE = 'percentage-only';
+ static const STATS_VALUE = 'value-only';
+ static const STATS_VALUE_PERCENTAGE = 'value-percentage';
+
+ final Iterable<int> dimensionsUsingBand = const[];
+ final statsMode;
+ final num innerRadius;
+
+ SelectionScope _scope;
+ List<List> _prevRows = null;
+ double _prevSlice;
+
+ PieChartRenderer({this.innerRadius: 0, this.statsMode: STATS_PERCENTAGE});
+
+ /*
+ * Returns false if the number of dimension axes != 0. Pie chart can only
+ * be rendered on areas with no axes.
+ */
+ @override
+ bool prepare(ChartArea area, ChartSeries series) {
+ _ensureAreaAndSeries(area, series);
+ return area.dimensionAxesCount == 0;
+ }
+
+ @override
+ void draw(GElement element) {
+ _ensureReadyToDraw(element);
+
+ var rows = new List()..addAll(area.data.rows.map((e) {
+ var row = [];
+ for (var measure in series.measures) {
+ row.add(e[measure]);
+ }
+ return row;
+ }));
+
+ var radius = math.min(rect.width, rect.height) / 2;
+ var outerRadius = radius - 10;
+ var sliceRadius = (radius - 10 - innerRadius) / rows.length;
+
+ if (_prevRows == null) {
+ _prevRows = new List<List>();
+ _prevSlice = sliceRadius;
+ }
+
+ while (_prevRows.length < rows.length)
+ _prevRows.add(new List());
+ while (_prevRows.length > rows.length)
+ _prevRows.removeLast();
+ for (int i = 0; i < _prevRows.length; i++) {
+ while (_prevRows[i].length > rows[i].length)
+ _prevRows[i].removeLast();
+ while (_prevRows[i].length < rows[i].length)
+ _prevRows[i].add(0);
+ }
+
+ var group = root.selectAll('.row-group').data(rows);
+ group.enter.append('g')
+ ..classed('row-group')
+ ..attrWithCallback('data-row', (d, i, e) => i)
+ ..attrWithCallback('transform', (d, i, c) =>
+ 'translate(${rect.width / 2}, ${rect.height / 2})');
+ group.exit.remove();
+
+ var layout = new PieLayout();
+ var arc = new SvgArc();
+
+ List<List> prevArcData = new List<List>();
+ for (int i = 0; i < rows.length; i++) {
+ prevArcData.add(layout.layout(_prevRows[i]));
+ for (int j = 0; j < rows[i].length; j++) {
+ prevArcData[i][j].innerRadius =
+ outerRadius - _prevSlice * (i + 1);
+ if (prevArcData[i][j].innerRadius < 0)
+ prevArcData[i][j].innerRadius = 0;
+ prevArcData[i][j].outerRadius = outerRadius - _prevSlice * i;
+ if (prevArcData[i][j].outerRadius < prevArcData[i][j].innerRadius)
+ prevArcData[i][j].outerRadius = prevArcData[i][j].innerRadius;
+ }
+ }
+
+ List<List> arcData = new List<List>();
+ for (int i = 0; i < rows.length; i++) {
+ arcData.add(layout.layout(rows[i]));
+ for (int j = 0; j < rows[i].length; j++) {
+ arcData[i][j].innerRadius = outerRadius - sliceRadius * (i + 1) + 0.5;
+ arcData[i][j].outerRadius = outerRadius - sliceRadius * i;
+ }
+ }
+
+ var pie = group.selectAll('.pie-path')
+ .dataWithCallback((d, i, c) => prevArcData[i]);
+ pie.enter.append('path')
+ ..classed('pie-path')
+ ..attrWithCallback('fill', (d, i, e) => colorForKey(i))
+ ..attrWithCallback('d', (d, i, e) {
+ return arc.path(d, i, host);
+ })
+ ..attr('stroke-width', '1px')
+ ..style('stroke', "#ffffff");
+
+ pie.dataWithCallback((d, i, c) => arcData[i]);
+ pie.transition()
+ ..attrWithCallback('fill', (d, i, e) => colorForKey(i))
+ ..attrTween('d', (d, i, e) {
+ int o = ((outerRadius - d.outerRadius) / sliceRadius).round();
+ return (t) => arc.path(interpolateSvgArcData(
+ prevArcData[o][i], arcData[o][i])(t), i, host);
+ })
+ ..duration(theme.transitionDuration);
+ pie
+ ..on('click', (d, i, e) => _event(mouseClickController, d, i, e))
+ ..on('mouseover', (d, i, e) => _event(mouseOverController, d, i, e))
+ ..on('mouseout', (d, i, e) => _event(mouseOutController, d, i, e));
+ pie.exit.remove();
+
+ for (int i = 0; i < rows.length; i++) {
+ for (int j = 0; j < rows[i].length; j++)
+ _prevRows[i][j] = rows[i][j];
+ }
+
+ _prevSlice = sliceRadius;
+
+ List total = new List();
+ rows.forEach((d) {
+ var sum = 0;
+ d.forEach((e) => sum += e);
+ total.add(sum);
+ });
+
+ var ic = -1,
+ order = 0;
+ var statistic = group.selectAll('.statistic')
+ .dataWithCallback((d, i, c) => arcData[i]);
+
+ statistic.enter.append('text')
+ ..classed('statistic')
+ ..style('fill', 'white')
+ ..attrWithCallback('transform', (d, i, c) {
+ var offsets = arc.centroid(d, i, c);
+ return 'translate(${offsets[0]}, ${offsets[1]})';
+ })
+ ..attr('dy', '.35em')
+ ..style('text-anchor', 'middle');
+
+ statistic
+ ..textWithCallback((d, i, e) {
+ if (i <= ic) order++;
+ ic = i;
+ return _processSliceText(d.data, total[order]);
+ })
+ ..attr('opacity', '0')
+ ..style('pointer-events', 'none')
+ ..attrWithCallback('transform', (d, i, c) {
+ var offsets = arc.centroid(d, i, c);
+ return 'translate(${offsets[0]}, ${offsets[1]})';
+ });
+
+ statistic.transition()
+ ..attr('opacity', '1')
+ ..delay(theme.transitionDuration)
+ ..duration(theme.transitionDuration);
+
+ statistic.exit.remove();
+ }
+
+ @override
+ void dispose() {
+ if (root == null) return;
+ root.selectAll('.row-group').remove();
+ }
+
+ String _processSliceText(value, total) {
+ var significant = value * 100 / total >= 5;
+ if (statsMode == STATS_PERCENTAGE) {
+ return (significant) ? '${(value * 100 / total).toStringAsFixed(0)}%': '';
+ } else if (statsMode == STATS_VALUE) {
+ return (significant) ? value.toString() : '';
+ } else {
+ return (significant) ?
+ '${value} (${(value * 100 / total).toStringAsFixed(0)}%)': '';
+ }
+ }
+
+ @override
+ double get bandInnerPadding => 0.0;
+
+ @override
+ double get bandOuterPadding => 0.0;
+
+ @override
+ Extent get extent => const Extent(0, 100);
+
+ void _event(StreamController controller, data, int index, Element e) {
+ if (controller == null) return;
+ var rowStr = e.parent.dataset['row'];
+ var row = rowStr != null ? int.parse(rowStr) : null;
+ controller.add(
+ new _ChartEvent(scope.event, area, series, row, index, data.value));
+ }
+}
diff --git a/pub/charted/lib/charts/renderers/stackedbar_chart_renderer.dart b/pub/charted/lib/charts/renderers/stackedbar_chart_renderer.dart
new file mode 100644
index 0000000..0edd82d
--- /dev/null
+++ b/pub/charted/lib/charts/renderers/stackedbar_chart_renderer.dart
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class StackedBarChartRenderer extends BaseRenderer {
+ final Iterable<int> dimensionsUsingBand = const[0];
+
+ /*
+ * Returns false if the number of dimension axes on the area is 0.
+ * Otherwise, the first dimension scale is used to render the chart.
+ */
+ @override
+ bool prepare(ChartArea area, ChartSeries series) {
+ _ensureAreaAndSeries(area, series);
+ return area.dimensionAxesCount != 0;
+ }
+
+ @override
+ void draw(Element element) {
+ _ensureReadyToDraw(element);
+
+ var measuresCount = series.measures.length,
+ measureScale = area.measureScales(series).first,
+ dimensionScale = area.dimensionScales.first;
+
+ var rows = new List()
+ ..addAll(area.data.rows.map((e) {
+ var row = [];
+ for (var i = series.measures.length - 1; i >= 0; i--) {
+ row.add(e[series.measures.elementAt(i)]);
+ }
+ return row;
+ }));
+
+ // We support only one dimension, so always use the first one.
+ var x = area.data.rows.map(
+ (row) => row.elementAt(area.config.dimensions.first)).toList();
+
+ var group = root.selectAll('.row-group').data(rows);
+ group.enter.append('g')
+ ..classed('row-group')
+ ..attrWithCallback('transform', (d, i, c) =>
+ 'translate(${dimensionScale.apply(x[i])}, 0)');
+ group.exit.remove();
+
+ group.transition()
+ ..attrWithCallback('transform', (d, i, c) =>
+ 'translate(${dimensionScale.apply(x[i])}, 0)')
+ ..duration(theme.transitionDuration)
+ ..attrWithCallback('data-row', (d, i, e) => i);
+
+ /* TODO(prsd): Handle cases where x and y axes are swapped */
+ var bar = group.selectAll('.bar').dataWithCallback((d, i, c) => rows[i]);
+
+ var ic = -1,
+ order = 0,
+ prevY = new List();
+
+ prevY.add(0);
+ bar.each((d, i, e) {
+ if (i > ic) {
+ prevY[prevY.length - 1] = e.attributes['y'];
+ } else {
+ prevY.add(e.attributes['y']);
+ }
+ ic = i;
+ });
+
+ ic = 1000000000;
+ var enter = bar.enter.append('rect')
+ ..classed('bar')
+ ..styleWithCallback('fill', (d, i, c) => colorForKey(_reverseIdx(i)))
+ ..attr('width', dimensionScale.rangeBand - theme.defaultStrokeWidth)
+ ..attrWithCallback('y', (d, i, c) {
+ var tempY;
+ if (i <= ic && i > 0) {
+ tempY = prevY[order];
+ order++;
+ } else {
+ tempY = rect.height;
+ }
+ ic = i;
+ return tempY;
+ })
+ ..attr('height', 0)
+ ..on('click', (d, i, e) => _event(mouseClickController, d, i, e))
+ ..on('mouseover', (d, i, e) => _event(mouseOverController, d, i, e))
+ ..on('mouseout', (d, i, e) => _event(mouseOutController, d, i, e));
+
+ bar.transition()
+ ..styleWithCallback('fill', (d, i, c) => colorForKey(_reverseIdx(i)))
+ ..attr('width', dimensionScale.rangeBand - theme.defaultStrokeWidth)
+ ..duration(theme.transitionDuration);
+
+ var y = 0,
+ length = bar.length,
+ // Keeps track of heights of previously graphed bars. If all bars before
+ // current one have 0 height, the current bar doesn't need offset.
+ prevAllZeroHeight = true,
+ // Keeps track of the offset already exist in the previous bar, when the
+ // computed bar height is less than (theme.defaultSeparatorWidth +
+ // theme.defaultStrokeWidth), this height is already discounted, so the
+ // next bar's offset in height can be this much less than normal.
+ prevOffset = 0;
+
+ bar.transition()
+ ..attrWithCallback('y', (d, i, c) {
+ if (i == 0) y = measureScale.apply(0).round();
+ return (y -= (rect.height - measureScale.apply(d).round()));
+ })
+ ..attrWithCallback('height', (d, i, c) {
+ var ht = rect.height - measureScale.apply(d).round();
+ if (i != 0) {
+ // If previous bars has 0 height, don't offset for spacing
+ // If any of the previous bar has non 0 height, do the offset.
+ ht -= prevAllZeroHeight ? 1 :
+ (theme.defaultSeparatorWidth + theme.defaultStrokeWidth);
+ ht += prevOffset;
+ } else {
+ // When rendering next group of bars, reset prevZeroHeight.
+ prevOffset = 0;
+ prevAllZeroHeight = true;
+ ht -= 1;
+ // -1 so bar does not overlap x axis.
+ }
+ if (ht <= 0) {
+ prevOffset = prevAllZeroHeight ? 0 :
+ (theme.defaultSeparatorWidth + theme.defaultStrokeWidth) + ht;
+ ht = 0;
+ }
+ prevAllZeroHeight = (ht == 0) && prevAllZeroHeight;
+ return ht;
+ })
+ ..duration(theme.transitionDuration)
+ ..delay(50);
+
+ if (theme.defaultStrokeWidth > 0) {
+ enter.attr('stroke-width', '${theme.defaultStrokeWidth}px');
+ enter.styleWithCallback('stroke', (d, i, c) =>
+ colorForKey(_reverseIdx(i)));
+ bar.transition()
+ ..styleWithCallback('stroke', (d, i, c) => colorForKey(_reverseIdx(i)));
+ }
+
+ bar.exit.remove();
+ }
+
+ @override
+ double get bandInnerPadding =>
+ area.theme.dimensionAxisTheme.axisBandInnerPadding;
+
+ @override
+ Extent get extent {
+ assert(area != null && series != null);
+ var rows = area.data.rows,
+ max = rows[0][series.measures.first],
+ min = max;
+
+ rows.forEach((row) {
+ if (row[series.measures.first] < min)
+ min = row[series.measures.first];
+
+ var bar = 0;
+ series.measures.forEach((idx) {
+ bar += row[idx];
+ });
+ if (bar > max) max = bar;
+ });
+
+ return new Extent(min, max);
+ }
+
+ void _event(StreamController controller, data, int index, Element e) {
+ if (controller == null) return;
+ var rowStr = e.parent.dataset['row'];
+ var row = rowStr != null ? int.parse(rowStr) : null;
+ controller.add(new _ChartEvent(
+ scope.event, area, series, row, _reverseIdx(index), data));
+ }
+
+ // Because waterfall bar chart render the measures in reverse order to match
+ // the legend, we need to reverse the index for color and event.
+ int _reverseIdx(int index) => series.measures.length - 1 - index;
+}
diff --git a/pub/charted/lib/charts/renderers/waterfall_chart_renderer.dart b/pub/charted/lib/charts/renderers/waterfall_chart_renderer.dart
new file mode 100644
index 0000000..d169bc6
--- /dev/null
+++ b/pub/charted/lib/charts/renderers/waterfall_chart_renderer.dart
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class WaterfallChartRenderer extends BaseRenderer {
+ final Iterable<int> dimensionsUsingBand = const[0];
+
+ /*
+ * Returns false if the number of dimension axes on the area is 0.
+ * Otherwise, the first dimension scale is used to render the chart.
+ */
+ @override
+ bool prepare(ChartArea area, ChartSeries series) {
+ _ensureAreaAndSeries(area, series);
+ return area.dimensionAxesCount != 0 && area.data is WaterfallChartData;
+ }
+
+ @override
+ void draw(Element element) {
+ _ensureReadyToDraw(element);
+
+ var measuresCount = series.measures.length,
+ measureScale = area.measureScales(series).first,
+ dimensionScale = area.dimensionScales.first;
+
+ // We support only one dimension, so always use the first one.
+ var x = area.data.rows.map(
+ (row) => row.elementAt(area.config.dimensions.first)).toList();
+
+ List<Iterable> rows = new List()
+ ..addAll(area.data.rows.map((e) {
+ var row = [];
+ for (var i = measuresCount - 1; i >= 0; i--) {
+ row.add(e[series.measures.elementAt(i)]);
+ }
+ return row;
+ }));
+
+ // Pre-compute shift value on y-axis for non-base rows
+ var yShift = new List(),
+ runningTotal = 0;
+ for (int i = 0; i < rows.length; i++) {
+ var row = rows[i];
+ if (_isBaseRow(i)) {
+ runningTotal = 0;
+ }
+ yShift.add(runningTotal);
+ var bar = 0;
+ row.forEach((value) => bar += value);
+ runningTotal += bar;
+
+ // Handle Nagative incremental values:
+ if (row.any((value) => value < 0)) {
+ assert(row.every((value) => value <= 0));
+ for (int j = 0; j < row.length; j++) {
+ row[j] = 0 - row[j];
+ }
+ yShift[yShift.length - 1] += bar;
+ }
+ }
+
+ var group = root.selectAll('.row-group').data(rows);
+ group.enter.append('g')
+ ..classed('row-group')
+ ..attrWithCallback('transform', (d, i, c) =>
+ 'translate(${dimensionScale.apply(x[i])},'
+ '${measureScale.apply(yShift[i]).round() - rect.height})');
+ group.exit.remove();
+
+ group.transition()
+ ..attrWithCallback('transform', (d, i, c) =>
+ 'translate(${dimensionScale.apply(x[i])},'
+ '${measureScale.apply(yShift[i]).round() - rect.height})')
+ ..duration(theme.transitionDuration)
+ ..attrWithCallback('data-row', (d, i, e) => i);
+
+ /* TODO(prsd): Handle cases where x and y axes are swapped */
+ var bar = group.selectAll('.bar').dataWithCallback((d, i, c) => rows[i]);
+
+ var ic = -1,
+ order = 0,
+ prevY = new List()..add(0);
+
+ bar.each((d, i, e) {
+ if (i > ic) {
+ prevY[prevY.length - 1] = e.attributes['y'];
+ } else {
+ prevY.add(e.attributes['y']);
+ }
+ ic = i;
+ });
+
+ ic = 1000000000;
+ var enter = bar.enter.append('rect')
+ ..classed('bar')
+ ..styleWithCallback('fill', (d, i, c) => colorForKey(_reverseIdx(i)))
+ ..attr('width', dimensionScale.rangeBand - theme.defaultStrokeWidth)
+ ..attrWithCallback('y', (d, i, c) {
+ var tempY;
+ if (i <= ic && i > 0) {
+ tempY = prevY[order];
+ order++;
+ } else {
+ tempY = rect.height;
+ }
+ ic = i;
+ return tempY;
+ })
+ ..attr('height', 0)
+ ..on('click', (d, i, e) => _event(mouseClickController, d, i, e))
+ ..on('mouseover', (d, i, e) => _event(mouseOverController, d, i, e))
+ ..on('mouseout', (d, i, e) => _event(mouseOutController, d, i, e));
+
+ bar.transition()
+ ..styleWithCallback('fill', (d, i, c) => colorForKey(_reverseIdx(i)))
+ ..attr('width', dimensionScale.rangeBand - theme.defaultStrokeWidth)
+ ..duration(theme.transitionDuration);
+
+ var y = 0,
+ length = bar.length,
+ // Keeps track of heights of previously graphed bars. If all bars before
+ // current one have 0 height, the current bar doesn't need offset.
+ prevAllZeroHeight = true,
+ // Keeps track of the offset already exist in the previous bar, when the
+ // computed bar height is less than (theme.defaultSeparatorWidth +
+ // theme.defaultStrokeWidth), this height is already discounted, so the
+ // next bar's offset in height can be this much less than normal.
+ prevOffset = 0;
+
+ bar.transition()
+ ..attrWithCallback('y', (d, i, c) {
+ if (i == 0) y = measureScale.apply(0).round();
+ return (y -= (rect.height - measureScale.apply(d).round()));
+ })
+ ..attrWithCallback('height', (d, i, c) {
+ var ht = rect.height - measureScale.apply(d).round();
+ if (i != 0) {
+ // If previous bars has 0 height, don't offset for spacing
+ // If any of the previous bar has non 0 height, do the offset.
+ ht -= prevAllZeroHeight ? 1 :
+ (theme.defaultSeparatorWidth + theme.defaultStrokeWidth);
+ ht += prevOffset;
+ } else {
+ // When rendering next group of bars, reset prevZeroHeight.
+ prevOffset = 0;
+ prevAllZeroHeight = true;
+ ht -= 1;
+ // -1 so bar does not overlap x axis.
+ }
+ if (ht <= 0) {
+ prevOffset = prevAllZeroHeight ? 0 :
+ (theme.defaultSeparatorWidth + theme.defaultStrokeWidth) + ht;
+ ht = 0;
+ }
+ prevAllZeroHeight = (ht == 0) && prevAllZeroHeight;
+ return ht;
+ })
+ ..duration(theme.transitionDuration)
+ ..delay(50);
+
+ if (theme.defaultStrokeWidth > 0) {
+ enter.attr('stroke-width', '${theme.defaultStrokeWidth}px');
+ enter.styleWithCallback('stroke', (d, i, c) =>
+ colorForKey(_reverseIdx(i)));
+ bar.transition()
+ ..styleWithCallback('stroke', (d, i, c) => colorForKey(_reverseIdx(i)));
+ }
+
+ bar.exit.remove();
+ }
+
+ @override
+ double get bandInnerPadding =>
+ area.theme.dimensionAxisTheme.axisBandInnerPadding;
+
+ @override
+ Extent get extent {
+ assert(area != null && series != null);
+ var rows = area.data.rows,
+ max = rows[0][series.measures.first],
+ min = max,
+ runningTotal = 0;
+
+ for (int i = 0; i < rows.length; i++) {
+ var row = rows[i];
+ if (_isBaseRow(i)) {
+ runningTotal = 0;
+ }
+ series.measures.forEach((idx) {
+ runningTotal += row[idx];
+ });
+ if (runningTotal > max) max = runningTotal;
+ if (runningTotal < min) min = runningTotal;
+ }
+ return new Extent(min, max);
+ }
+
+ void _event(StreamController controller, data, int index, Element e) {
+ if (controller == null) return;
+ var rowStr = e.parent.dataset['row'];
+ var row = rowStr != null ? int.parse(rowStr) : null;
+ controller.add(new _ChartEvent(
+ scope.event, area, series, row, _reverseIdx(index), data));
+ }
+
+ // Because waterfall bar chart render the measures in reverse order to match
+ // the legend, we need to reverse the index for color and event.
+ int _reverseIdx(int index) => series.measures.length - 1 - index;
+
+ bool _isBaseRow(int index) =>
+ area.data is WaterfallChartData ?
+ (area.data as WaterfallChartData).baseRows.contains(index) : false;
+}
diff --git a/pub/charted/lib/charts/src/chart_area_impl.dart b/pub/charted/lib/charts/src/chart_area_impl.dart
new file mode 100644
index 0000000..995a3f5
--- /dev/null
+++ b/pub/charted/lib/charts/src/chart_area_impl.dart
@@ -0,0 +1,651 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Generic implementation of ChartArea.
+ *
+ * Assumes that the chart displays either one or two dimension axes and zero
+ * or more measure axis. The number of measure axes displayed is zero in charts
+ * similar to bubble chart - the number of dimension axes is two.
+ *
+ * The primary dimension axes is always at the bottom, the primary measure axis
+ * is always on the right.
+ */
+class _ChartArea implements ChartArea {
+ static const List MEASURE_AXIS_IDS = const['_default'];
+ static const List DIMENSION_AXIS_IDS = const['_primary', '_secondary'];
+
+ static const List MEASURE_AXIS_ORIENTATIONS =
+ const[ORIENTATION_LEFT, ORIENTATION_RIGHT];
+ static const List MEASURE_AXIS_ORIENTATIONS_ALT =
+ const[ORIENTATION_BOTTOM, ORIENTATION_TOP];
+
+ static const List DIMENSION_AXIS_ORIENTATIONS =
+ const[ORIENTATION_BOTTOM, ORIENTATION_LEFT];
+ static const List DIMENSION_AXIS_ORIENTATIONS_ALT =
+ const[ORIENTATION_LEFT, ORIENTATION_BOTTOM];
+
+ static const int MEASURE_AXES_COUNT = 2;
+ static const int MARGIN = 10;
+
+ final LinkedHashMap<String, _ChartAxis> _measureAxes = new LinkedHashMap();
+ final LinkedHashMap<int, _ChartAxis> _dimensionAxes = new LinkedHashMap();
+ final HashSet<int> dimensionsUsingBands = new HashSet();
+ final SubscriptionsDisposer _dataEventsDisposer = new SubscriptionsDisposer();
+ final SubscriptionsDisposer _configEventsDisposer =
+ new SubscriptionsDisposer();
+
+ final Element _host;
+
+ List<ChartBehavior> _behaviors = new List();
+ Map<ChartSeries, _ChartSeriesInfo> _seriesInfoCache = new Map();
+
+ ChartTheme theme;
+ bool autoUpdate = false;
+
+ ChartData _data;
+ ChartConfig _config;
+ int _dimensionAxesCount;
+
+ _ChartAreaLayout layout = new _ChartAreaLayout();
+ SelectionScope _scope;
+ Selection _svg;
+ Selection _group;
+ Iterable<ChartSeries> _series;
+ bool _pendingLegendUpdate = false;
+
+ StreamController<ChartEvent> _valueMouseOverController;
+ StreamController<ChartEvent> _valueMouseOutController;
+ StreamController<ChartEvent> _valueMouseClickController;
+
+ @override
+ Element upperBehaviorPane;
+
+ @override
+ Element lowerBehaviorPane;
+
+ _ChartArea(Element this._host, ChartData data, ChartConfig config,
+ bool this.autoUpdate, this._dimensionAxesCount) {
+ assert(_host != null);
+ assert(isNotInline(_host));
+
+ this.data = data;
+ this.config = config;
+ theme = ChartTheme.current;
+
+
+ Transition.defaultEasingType = theme.transitionEasingType;
+ Transition.defaultEasingMode = theme.transitionEasingMode;
+ Transition.defaultDuration = theme.transitionDuration;
+ }
+
+ void dispose() {
+ _configEventsDisposer.dispose();
+ _dataEventsDisposer.dispose();
+ }
+
+ static bool isNotInline(Element e) =>
+ e != null && e.getComputedStyle().display != 'inline';
+
+ Element get host => _host;
+
+ /*
+ * If [value] is [Observable], subscribes to changes and updates the
+ * chart when data changes.
+ */
+ @override
+ set data(ChartData value) {
+ _data = value;
+ _dataEventsDisposer.dispose();
+
+ if (autoUpdate && _data != null && _data is Observable) {
+ _dataEventsDisposer.add((_data as Observable).changes.listen((_) {
+ _pendingLegendUpdate = (_data is TransposeTransformer);
+ draw();
+ }));
+ }
+ }
+
+ @override
+ ChartData get data => _data;
+
+ /*
+ * If [value] is [Observable], subscribes to changes and updates the
+ * chart when series or dimensions change in configuration.
+ */
+ @override
+ set config(ChartConfig value) {
+ _config = value;
+ _configEventsDisposer.dispose();
+ _pendingLegendUpdate = true;
+
+ if (_config != null) {
+ _configEventsDisposer.add((_config as Observable).changes.listen((_) {
+ _pendingLegendUpdate = true;
+ draw();
+ }));
+ }
+ }
+
+ @override
+ ChartConfig get config => _config;
+
+ /*
+ * Number of dimension axes displayed in this chart.
+ */
+ @override
+ set dimensionAxesCount(int count) {
+ _dimensionAxesCount = count;
+ if (autoUpdate) draw();
+ }
+
+ @override
+ int get dimensionAxesCount => _dimensionAxesCount;
+
+ /*
+ * Gets measure axis from cache - creates a new instance of _ChartAxis
+ * if one was not already created for the given axis [id].
+ */
+ _ChartAxis _getMeasureAxis(String id) {
+ _measureAxes.putIfAbsent(id, () {
+ var axisConf = config.getMeasureAxis(id),
+ axis = axisConf != null ?
+ new _ChartAxis.withAxisConfig(this, axisConf) :
+ new _ChartAxis(this);
+ return axis;
+ });
+ return _measureAxes[id];
+ }
+
+ /*
+ * Gets a dimension axis from cache - creates a new instance of _ChartAxis
+ * if one was not already created for the given dimension [column].
+ */
+ _ChartAxis _getDimensionAxis(int column) {
+ _dimensionAxes.putIfAbsent(column, () {
+ var axisConf = config.getDimensionAxis(column),
+ axis = axisConf != null ?
+ new _ChartAxis.withAxisConfig(this, axisConf) :
+ new _ChartAxis(this);
+ return axis;
+ });
+ return _dimensionAxes[column];
+ }
+
+ /*
+ * All columns rendered by a series must be of the same type and the
+ * series must use a renderer that supports current [ChartArea]
+ * configuration.
+ */
+ bool _isSeriesValid(ChartSeries s) {
+ var first = data.columns.elementAt(s.measures.first).type;
+ return s.measures.every((i) =>
+ (i < data.columns.length) && data.columns.elementAt(i).type == first);
+ }
+
+ /*
+ * Indicates if the given ChartSeries needs an ordinal scale
+ */
+ bool _isOrdinalColumn(column) =>
+ column.useOrdinalScale == true ||
+ (column.useOrdinalScale == null &&
+ ChartColumnSpec.ORDINAL_SCALES.contains(column.type));
+
+ /*
+ * Get a list of dimension scales for this chart.
+ */
+ @override
+ Iterable<Scale> get dimensionScales =>
+ config.dimensions.map((int column) => _getDimensionAxis(column).scale);
+
+ /*
+ * Get a list of scales used by [series]
+ */
+ @override
+ Iterable<Scale> measureScales(ChartSeries series) {
+ var axisIds = isNullOrEmpty(series.measureAxisIds) ?
+ MEASURE_AXIS_IDS : series.measureAxisIds;
+ return axisIds.map((String id) => _getMeasureAxis(id).scale);
+ }
+
+ /*
+ * Computes the size of chart and if changed from the previous time
+ * size was computed, sets attributes on svg element
+ */
+ Rect _computeChartSize() {
+ int width = host.clientWidth,
+ height = host.clientHeight;
+
+ if (config.minimumSize != null) {
+ width = max([width, config.minimumSize.width]);
+ height = max([height, config.minimumSize.height]);
+ }
+
+ Rect current =
+ new Rect(MARGIN, MARGIN, width - 2 * MARGIN, height - 2 * MARGIN);
+ if (layout.chartArea == null || layout.chartArea != current) {
+ _svg.attr('width', width.toString());
+ _svg.attr('height', height.toString());
+ _group.attr('transform', 'translate($MARGIN, $MARGIN)');
+ layout.chartArea = current;
+ }
+ return layout.chartArea;
+ }
+
+ @override
+ draw() {
+ assert(data != null && config != null);
+ assert(config.series != null && config.series.isNotEmpty);
+
+ /* Create SVG element and other one-time initializations. */
+ if (_scope == null) {
+ _scope = new SelectionScope.element(host);
+ _svg = _scope.append('svg:svg')..classed('charted-chart');
+ _group = _svg.append('g')..classed('chart-wrapper');
+
+ /* Create groups for behaviors to add any SVG elements */
+ var lower = _group.append('g')..classed('lower-render-pane'),
+ upper = _group.append('g')..classed('upper-first-pane');
+
+ lowerBehaviorPane = lower.first;
+ upperBehaviorPane = upper.first;
+ if (_behaviors.isNotEmpty) {
+ _behaviors.forEach(
+ (b) => b.init(this, upperBehaviorPane, lowerBehaviorPane));
+ }
+ }
+
+ /* Compute sizes and filter out unsupported series */
+ var size = _computeChartSize(),
+ series = config.series.where((s) =>
+ _isSeriesValid(s) && s.renderer.prepare(this, s)),
+ selection = _group.selectAll('.series-group').
+ data(series, (x) => x.hashCode),
+ axesDomainCompleter = new Completer();
+
+ /*
+ * Wait till the axes are rendered before rendering series.
+ * In an SVG, z-index is based on the order of nodes in the DOM.
+ */
+ axesDomainCompleter.future.then((_) {
+ /* If a series was not rendered before, add an SVG group for it */
+ selection.enter.append('svg:g')
+ ..classed('series-group');
+
+ /* For all series recompute axis ranges and update the rendering */
+ var transform =
+ 'translate(${layout.renderArea.x},${layout.renderArea.y})';
+ selection.each((ChartSeries s, _, Element group) {
+ var info = _seriesInfoCache[s];
+ if (info == null) {
+ info = _seriesInfoCache[s] = new _ChartSeriesInfo(this, s);
+ }
+ info.check();
+ group.attributes['transform'] = transform;
+ s.renderer.draw(group);
+ });
+
+ /* A series that was rendered earlier isn't there anymore, remove it */
+ selection.exit
+ ..each((ChartSeries s, _, __) {
+ var info = _seriesInfoCache[s];
+ if (info != null) info.dispose();
+ _seriesInfoCache.remove(s);
+ })
+ ..remove();
+ });
+
+ // Save the list of valid series for use with legend and axes.
+ _series = series;
+
+ // If we have atleast one dimension axis, render the axes.
+ if (dimensionAxesCount != 0) {
+ _initAxes();
+ } else {
+ _computeLayoutWithoutAxes();
+ }
+
+ // Render the chart, now that the axes layer is already in DOM.
+ axesDomainCompleter.complete();
+
+ // Updates the legend if required.
+ _updateLegend();
+ }
+
+ _initAxes() {
+ var measureAxisUsers = {};
+
+ /* Create necessary measures axes */
+ _series.forEach((ChartSeries s) {
+ var ids = isNullOrEmpty(s.measureAxisIds) ?
+ MEASURE_AXIS_IDS : s.measureAxisIds;
+ ids.forEach((id) {
+ var axis = _getMeasureAxis(id),
+ users = measureAxisUsers[id];
+ if (users == null) {
+ measureAxisUsers[id] = [s];
+ } else {
+ users.add(s);
+ }
+ });
+ });
+
+ /* Configure measure axes */
+ measureAxisUsers.forEach((id, listOfSeries) {
+ var sampleCol = listOfSeries.first.measures.first,
+ sampleColSpec = data.columns.elementAt(sampleCol),
+ axis = _getMeasureAxis(id),
+ domain;
+
+ if (sampleColSpec.useOrdinalScale) {
+ /* TODO(prsd): Ordinal measure scale */
+ } else {
+ var lowest = min(listOfSeries.map((s) => s.renderer.extent.min)),
+ highest = max(listOfSeries.map((s) => s.renderer.extent.max));
+
+ // Use default domain if lowest and highest are the same, right now
+ // lowest is always 0, change to lowest when we make use of it.
+ domain = (highest != 0) ? [0, highest] : [0, 1];
+ }
+ axis.initAxisDomain(sampleCol, false, domain);
+ });
+
+ /* Configure dimension axes */
+ config.dimensions.take(dimensionAxesCount).forEach((int column) {
+ var axis = _getDimensionAxis(column),
+ sampleColumnSpec = data.columns.elementAt(column),
+ values = data.rows.map((row) => row.elementAt(column)),
+ domain;
+
+ if (sampleColumnSpec.useOrdinalScale) {
+ domain = values.map((e) => e.toString()).toList();
+ } else {
+ var extent = new Extent.items(values);
+ domain = [extent.min, extent.max];
+ }
+ axis.initAxisDomain(column, true, domain);
+ });
+
+ /* Build a list of dimension axes that use range bands */
+ dimensionsUsingBands.clear();
+ _series.forEach((ChartSeries s) =>
+ dimensionsUsingBands.addAll(s.renderer.dimensionsUsingBand.map((i) =>
+ config.dimensions.elementAt(i))));
+
+ /* List of measure and dimension axes that are displayed */
+ var measureAxesCount = dimensionAxesCount == 1 ? MEASURE_AXES_COUNT : 0,
+ displayedMeasureAxes = (config.displayedMeasureAxes == null ?
+ _measureAxes.keys.take(measureAxesCount) :
+ config.displayedMeasureAxes.take(measureAxesCount)).
+ toList(growable:false),
+ displayedDimensionAxes =
+ config.dimensions.take(dimensionAxesCount).toList(growable:false);
+
+ /* Compute size of the dimension axes */
+ if (config.renderDimensionAxes != false) {
+ var dimensionAxisOrientations = config.leftAxisIsPrimary ?
+ DIMENSION_AXIS_ORIENTATIONS_ALT : DIMENSION_AXIS_ORIENTATIONS;
+ displayedDimensionAxes.asMap().forEach((int index, int column) {
+ var axis = _dimensionAxes[column],
+ orientation = dimensionAxisOrientations[index];
+ axis.prepareToDraw(orientation, theme.dimensionAxisTheme);
+ layout._axes[orientation] = axis.size;
+ });
+ }
+
+ /* Compute size of the measure axes */
+ if (displayedMeasureAxes.isNotEmpty) {
+ var measureAxisOrientations = config.leftAxisIsPrimary ?
+ MEASURE_AXIS_ORIENTATIONS_ALT : MEASURE_AXIS_ORIENTATIONS;
+ displayedMeasureAxes.asMap().forEach((int index, String key) {
+ var axis = _measureAxes[key],
+ orientation = measureAxisOrientations[index];
+ axis.prepareToDraw(orientation, theme.measureAxisTheme);
+ layout._axes[orientation] = axis.size;
+ });
+ }
+
+ _computeLayoutWithAxes();
+
+ /* Initialize output range on the invisible measure axes */
+ if (_measureAxes.length != displayedMeasureAxes.length) {
+ _measureAxes.keys.forEach((String axisId) {
+ if (displayedMeasureAxes.contains(axisId)) return;
+ _getMeasureAxis(axisId).initAxisScale(
+ [layout.renderArea.height, 0], theme.measureAxisTheme);
+ });
+ }
+
+ /* Display measure axes if we need to */
+ if (displayedMeasureAxes.isNotEmpty) {
+ var axisGroups =
+ _group.selectAll('.measure-group').data(displayedMeasureAxes);
+
+ /* Update measure axis (add/remove/update) */
+ axisGroups.enter.append('svg:g');
+ axisGroups
+ ..each((axisId, index, group) {
+ _getMeasureAxis(axisId).draw(group);
+ group.classes.clear();
+ group.classes.addAll(['measure-group','measure-${index}']);
+ });
+ axisGroups.exit.remove();
+ }
+
+ if (config.renderDimensionAxes != false) {
+ /* Display the dimension axes */
+ var dimAxisGroups =
+ _group.selectAll('.dim-group').data(displayedDimensionAxes);
+
+ /* Update dimension axes (add new / remove old / update remaining) */
+ dimAxisGroups.enter.append('svg:g');
+ dimAxisGroups
+ ..each((column, index, group) {
+ _getDimensionAxis(column).draw(group);
+ group.classes.clear();
+ group.classes.addAll(['dim-group', 'dim-${index}']);
+ });
+ dimAxisGroups.exit.remove();
+ } else {
+ /* Initialize output range on the invisible axis */
+ var dimensionAxisOrientations = config.leftAxisIsPrimary ?
+ DIMENSION_AXIS_ORIENTATIONS_ALT : DIMENSION_AXIS_ORIENTATIONS;
+ for (int i = 0; i < dimensionAxesCount; i++) {
+ var column = config.dimensions.elementAt(i),
+ axis = _dimensionAxes[column],
+ orientation = dimensionAxisOrientations[i];
+ axis.initAxisScale(orientation == ORIENTATION_LEFT ?
+ [layout.renderArea.height, 0] : [0, layout.renderArea.width],
+ theme.dimensionAxisTheme);
+ };
+ }
+ }
+
+ _computeLayoutWithoutAxes() {
+ layout.renderArea =
+ new Rect(0, 0, layout.chartArea.width, layout.chartArea.height);
+ }
+
+ /* Compute chart render area size and positions of all elements */
+ _computeLayoutWithAxes() {
+ var topAxis = layout.axes[ORIENTATION_TOP],
+ leftAxis = layout.axes[ORIENTATION_LEFT],
+ bottomAxis = layout.axes[ORIENTATION_BOTTOM],
+ rightAxis = layout.axes[ORIENTATION_RIGHT],
+ renderAreaHeight = layout.chartArea.height -
+ (topAxis.height + layout.axes[ORIENTATION_BOTTOM].height),
+ renderAreaWidth = layout.chartArea.width -
+ (leftAxis.width + layout.axes[ORIENTATION_RIGHT].width);
+
+ layout.renderArea = new Rect(
+ leftAxis.width, topAxis.height, renderAreaWidth, renderAreaHeight);
+
+ layout._axes
+ ..[ORIENTATION_TOP] =
+ new Rect(leftAxis.width, 0, renderAreaWidth, topAxis.height)
+ ..[ORIENTATION_RIGHT] =
+ new Rect(leftAxis.width + renderAreaWidth, topAxis.y,
+ rightAxis.width, renderAreaHeight)
+ ..[ORIENTATION_BOTTOM] =
+ new Rect(leftAxis.width, topAxis.height + renderAreaHeight,
+ renderAreaWidth, bottomAxis.height)
+ ..[ORIENTATION_LEFT] =
+ new Rect(leftAxis.width, topAxis.height,
+ leftAxis.width, renderAreaHeight);
+ }
+
+ /*
+ * Updates the legend, if configuration changed since the last
+ * time the legend was updated.
+ */
+ _updateLegend() {
+ if (!_pendingLegendUpdate) return;
+ if (_config == null || _config.legend == null || _series.isEmpty) return;
+
+ var legend = <ChartLegendItem>[];
+ List seriesByColumn =
+ new List.generate(data.columns.length, (i) => new List());
+
+ _series.forEach((s) =>
+ s.measures.forEach((m) => seriesByColumn[m].add(s)));
+
+ seriesByColumn.asMap().forEach((int i, List s) {
+ if (s.length == 0) return;
+ legend.add(new ChartLegendItem(
+ column:i, label:data.columns.elementAt(i).label, series:s,
+ color:theme.getColorForKey(i)));
+ });
+
+ _config.legend.update(legend, this);
+ _pendingLegendUpdate = false;
+ }
+
+ @override
+ Stream<ChartEvent> get onMouseUp =>
+ host.onMouseUp
+ .map((MouseEvent e) => new _ChartEvent(e, this));
+
+ @override
+ Stream<ChartEvent> get onMouseDown =>
+ host.onMouseDown
+ .map((MouseEvent e) => new _ChartEvent(e, this));
+
+ @override
+ Stream<ChartEvent> get onMouseOver =>
+ host.onMouseOver
+ .map((MouseEvent e) => new _ChartEvent(e, this));
+
+ @override
+ Stream<ChartEvent> get onMouseOut =>
+ host.onMouseOut
+ .map((MouseEvent e) => new _ChartEvent(e, this));
+
+ @override
+ Stream<ChartEvent> get onMouseMove =>
+ host.onMouseMove
+ .map((MouseEvent e) => new _ChartEvent(e, this));
+
+ @override
+ Stream<ChartEvent> get onValueClick {
+ if (_valueMouseClickController == null) {
+ _valueMouseClickController = new StreamController.broadcast(sync: true);
+ }
+ return _valueMouseClickController.stream;
+ }
+
+ @override
+ Stream<ChartEvent> get onValueMouseOver {
+ if (_valueMouseOverController == null) {
+ _valueMouseOverController = new StreamController.broadcast(sync: true);
+ }
+ return _valueMouseOverController.stream;
+ }
+
+ @override
+ Stream<ChartEvent> get onValueMouseOut {
+ if (_valueMouseOutController == null) {
+ _valueMouseOutController = new StreamController.broadcast(sync: true);
+ }
+ return _valueMouseOutController.stream;
+ }
+
+ @override
+ void addChartBehavior(ChartBehavior behavior) {
+ if (behavior == null || _behaviors.contains(behavior)) return;
+ _behaviors.add(behavior);
+ if (upperBehaviorPane != null && lowerBehaviorPane != null) {
+ behavior.init(this, upperBehaviorPane, lowerBehaviorPane);
+ }
+ }
+
+ @override
+ void removeChartBehavior(ChartBehavior behavior) {
+ if (behavior == null || !_behaviors.contains(behavior)) return;
+ if (upperBehaviorPane != null && lowerBehaviorPane != null) {
+ behavior.dispose();
+ }
+ _behaviors.remove(behavior);
+ }
+}
+
+class _ChartAreaLayout implements ChartAreaLayout {
+ @override
+ final _axes = <String, Rect>{
+ ORIENTATION_LEFT: const Rect(),
+ ORIENTATION_RIGHT: const Rect(),
+ ORIENTATION_TOP: const Rect(),
+ ORIENTATION_BOTTOM: const Rect()
+ };
+
+ UnmodifiableMapView<String, Rect> _axesView;
+
+ get axes => _axesView;
+
+ @override
+ Rect renderArea;
+
+ @override
+ Rect chartArea;
+
+ _ChartAreaLayout() {
+ _axesView = new UnmodifiableMapView(_axes);
+ }
+}
+
+class _ChartSeriesInfo {
+ ChartRenderer _renderer;
+ SubscriptionsDisposer _disposer = new SubscriptionsDisposer();
+
+ _ChartSeries _series;
+ _ChartArea _area;
+ _ChartSeriesInfo(this._area, this._series);
+
+ _event(StreamController controller, ChartEvent evt) {
+ if (controller == null) return;
+ controller.add(evt);
+ }
+
+ check() {
+ if (_renderer != _series.renderer) dispose();
+ _renderer = _series.renderer;
+ try {
+ _disposer.addAll([
+ _renderer.onValueMouseClick.listen(
+ (ChartEvent e) => _event(_area._valueMouseClickController, e)),
+ _renderer.onValueMouseOver.listen(
+ (ChartEvent e) => _event(_area._valueMouseOverController, e)),
+ _renderer.onValueMouseOut.listen(
+ (ChartEvent e) => _event(_area._valueMouseOutController, e))
+ ]);
+ } on UnimplementedError {};
+ }
+
+ dispose() => _disposer.dispose();
+}
diff --git a/pub/charted/lib/charts/src/chart_axis_impl.dart b/pub/charted/lib/charts/src/chart_axis_impl.dart
new file mode 100644
index 0000000..688f4a2
--- /dev/null
+++ b/pub/charted/lib/charts/src/chart_axis_impl.dart
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class _ChartAxis {
+ static const List _VERTICAL_ORIENTATIONS =
+ const [ ORIENTATION_LEFT, ORIENTATION_RIGHT ];
+
+ _ChartArea area;
+ ChartAxisConfig config;
+ ChartAxisTheme _theme;
+
+ int _column;
+ Iterable _domain;
+ bool _isDimension;
+ ChartColumnSpec _columnSpec;
+
+ GElement _element;
+ SvgAxis _axis;
+
+ bool _isVertical;
+ String _orientation;
+ Scale _scale;
+ SelectionScope _scope;
+ Selection _group;
+
+ EditableRect size;
+
+ _ChartAxis.withAxisConfig(this.area, this.config);
+ _ChartAxis(this.area);
+
+ void initAxisDomain(int column, bool isDimension, Iterable domain) {
+ _columnSpec = area.data.columns.elementAt(column);
+ _column = column;
+ _domain = domain;
+ _isDimension = isDimension;
+ if (scale == null) _scale = _columnSpec.createDefaultScale();
+ }
+
+ void initAxisScale(Iterable range, ChartAxisTheme theme) {
+ assert(scale != null);
+
+ // Sets the domain if not using a custom scale.
+ if (config == null || (config != null && config.scale == null)) {
+ scale.domain = _domain;
+ scale.nice(theme.axisTickCount);
+ }
+
+ // Sets the range if not using a custom scale, or the custom scale uses
+ // default range.
+ /* TODO(prsd): Scale needs some cleanup */
+ if (scale is OrdinalScale) {
+ var usingBands = area.dimensionsUsingBands.contains(_column),
+ innerPadding = usingBands ? theme.axisBandInnerPadding : 1.0,
+ outerPadding = usingBands ?
+ theme.axisBandOuterPadding : theme.axisOuterPadding;
+ (scale as OrdinalScale).
+ rangeRoundBands(range, innerPadding, outerPadding);
+ } else {
+ scale.range = range;
+ }
+ }
+
+ void prepareToDraw(String orientation, ChartAxisTheme theme) {
+ if (orientation == null) orientation = ORIENTATION_BOTTOM;
+
+ _theme = theme;
+ _orientation = orientation;
+ _isVertical = _orientation == ORIENTATION_LEFT ||
+ _orientation == ORIENTATION_RIGHT;
+
+ if (false && _theme.axisAutoResize) {
+ /* TODO(prsd): Implement axis size computations */
+ } else {
+ var width = _isVertical ?
+ _theme.verticalAxisWidth : area.layout.chartArea.width;
+ var height = _isVertical ?
+ area.layout.chartArea.height : _theme.horizontalAxisHeight;
+ size = new EditableRect.size(width, height);
+ }
+ }
+
+ void draw(GElement element) {
+ assert(element != null && element is GElement);
+ assert(scale != null);
+
+ var rect = area.layout.axes[_orientation],
+ renderAreaRect = area.layout.renderArea,
+ range = _isVertical ? [rect.height, 0] : [0, rect.width],
+ className = (_isVertical ? 'vertical-axis': 'horizontal-axis');
+
+ element.attributes['transform'] = 'translate(${rect.x}, ${rect.y})';
+
+ if (_axis == null || _element != element) {
+ _element = element;
+ _axis = new SvgAxis()
+ ..orientation = _orientation
+ ..suggestedTickCount = _theme.axisTickCount
+ ..tickPadding = _theme.axisTickPadding
+ ..outerTickSize = 0
+ ..tickFormat = _columnSpec.formatter;
+
+ if (config != null && config.tickValues != null) {
+ _axis.tickValues = config.tickValues;
+ }
+
+ _scope = new SelectionScope.element(_element);
+ _group = _scope.selectElements([_element]);
+ }
+
+ _axis.innerTickSize = _theme.axisTickSize;
+ if (_axis.innerTickSize <= ChartAxisTheme.FILL_RENDER_AREA) {
+ _axis.innerTickSize =
+ 0 - (_isVertical ? renderAreaRect.width : renderAreaRect.height);
+ }
+ initAxisScale(range, _theme);
+ if (_axis.scale != scale) _axis.scale = scale;
+ _axis.axis(_group);
+ }
+
+ void clear() {
+ }
+
+ // Scale passed through configuration takes precedence
+ Scale get scale =>
+ (config != null && config.scale != null) ? config.scale : _scale;
+
+ set scale(Scale value) => _scale = value;
+}
diff --git a/pub/charted/lib/charts/src/chart_config_impl.dart b/pub/charted/lib/charts/src/chart_config_impl.dart
new file mode 100644
index 0000000..bb4364b
--- /dev/null
+++ b/pub/charted/lib/charts/src/chart_config_impl.dart
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class _ChartConfig extends ChangeNotifier implements ChartConfig {
+ final Map<String,ChartAxisConfig> _measureAxisRegistry = {};
+ final Map<int,ChartAxisConfig> _dimensionAxisRegistry = {};
+ final SubscriptionsDisposer _disposer = new SubscriptionsDisposer();
+
+ Iterable<ChartSeries> _series;
+ Iterable<int> _dimensions;
+ StreamSubscription _dimensionsSubscription;
+
+ @override
+ Rect minimumSize = const Rect.size(400, 300);
+
+ @override
+ bool leftAxisIsPrimary = false;
+
+ @override
+ bool autoResizeAxis = true;
+
+ @override
+ ChartLegend legend;
+
+ @override
+ Iterable<String> displayedMeasureAxes;
+
+ @override
+ bool renderDimensionAxes = true;
+
+ _ChartConfig(Iterable<ChartSeries> series, Iterable<int> dimensions) {
+ this.series = series;
+ this.dimensions = dimensions;
+ }
+
+ @override
+ set series(Iterable<ChartSeries> values) {
+ assert(values != null && values.isNotEmpty);
+
+ _disposer.dispose();
+ _series = values;
+ notifyChange(const ChartConfigChangeRecord());
+
+ // Monitor each series for changes on them
+ values.forEach((item) => _disposer.add(item.changes.listen(
+ (_) => notifyChange(const ChartConfigChangeRecord())), item));
+
+ // Monitor series for changes. When the list changes, update
+ // subscriptions to ChartSeries changes.
+ if (_series is ObservableList) {
+ var observable = _series as ObservableList;
+ _disposer.add(observable.listChanges.listen((records) {
+ records.forEach((record) {
+ record.removed.forEach((value) => _disposer.unsubscribe(value));
+ for (int i = 0; i < record.addedCount; i++) {
+ var added = observable[i + record.index];
+ _disposer.add(added.changes.listen(
+ (_) => notifyChange(const ChartConfigChangeRecord())));
+ }
+ });
+ notifyChange(const ChartConfigChangeRecord());
+ }));
+ }
+ }
+
+ @override
+ Iterable<ChartSeries> get series => _series;
+
+ @override
+ set dimensions(Iterable<int> values) {
+ _dimensions = values;
+
+ if (_dimensionsSubscription != null) {
+ _dimensionsSubscription.cancel();
+ _dimensionsSubscription = null;
+ }
+
+ if (values == null || values.isEmpty) return;
+
+ if (_dimensions is ObservableList) {
+ _dimensionsSubscription =
+ (_dimensions as ObservableList).listChanges.listen(
+ (_) => notifyChange(const ChartConfigChangeRecord()));
+ }
+ }
+
+ @override
+ Iterable<int> get dimensions => _dimensions;
+
+ @override
+ void registerMeasureAxis(String id, ChartAxisConfig config) {
+ assert(config != null);
+ _measureAxisRegistry[id] = config;
+ }
+
+ @override
+ ChartAxisConfig getMeasureAxis(String id) => _measureAxisRegistry[id];
+
+ @override
+ void registerDimensionAxis(int column, ChartAxisConfig config) {
+ assert(config != null);
+ assert(dimensions.contains(column));
+ _dimensionAxisRegistry[column] = config;
+ }
+
+ @override
+ ChartAxisConfig getDimensionAxis(int column) =>
+ _dimensionAxisRegistry[column];
+}
diff --git a/pub/charted/lib/charts/src/chart_data_impl.dart b/pub/charted/lib/charts/src/chart_data_impl.dart
new file mode 100644
index 0000000..723b8eb
--- /dev/null
+++ b/pub/charted/lib/charts/src/chart_data_impl.dart
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class _ChartData extends ChangeNotifier implements ChartData {
+ Iterable<ChartColumnSpec> _columns;
+ Iterable<Iterable> _rows;
+
+ bool _hasObservableRows = false;
+ SubscriptionsDisposer _disposer = new SubscriptionsDisposer();
+
+ _ChartData(Iterable<ChartColumnSpec> columns, Iterable<Iterable> rows) {
+ this.columns = columns;
+ this.rows = rows;
+ }
+
+ set columns(Iterable<ChartColumnSpec> value) {
+ assert(value != null && value.isNotEmpty);
+
+ // Create a copy of columns. We do not currently support
+ // changes to the list of columns. Any changes to the spec
+ // will be applied at the next ChartBase.draw();
+ this._columns = new List<ChartColumnSpec>.from(value);
+ }
+
+ Iterable<ChartColumnSpec> get columns => _columns;
+
+ set rows(Iterable<Iterable> value) {
+ assert(value != null);
+
+ _rows = value;
+ if (_rows is ObservableList) {
+ _disposer.add(
+ (_rows as ObservableList).listChanges.listen(rowsChanged));
+ }
+
+ if (_rows.every((row) => row is ObservableList)) {
+ _hasObservableRows = true;
+ for (int i = 0; i < _rows.length; i++) {
+ var row = _rows.elementAt(i);
+ _disposer.add(row.listChanges.listen((changes)
+ => _valuesChanged(i, changes)), row);
+ };
+ } else if (_rows is Observable) {
+ logger.info('List of rows is Observable, but not rows themselves!');
+ }
+ }
+
+ Iterable<Iterable> get rows => _rows;
+
+ rowsChanged(List<ListChangeRecord> changes) {
+ if (_rows is! ObservableList) return;
+ notifyChange(new ChartRowChangeRecord(changes));
+
+ if (!_hasObservableRows) return;
+ changes.forEach((ListChangeRecord change) {
+ change.removed.forEach((item) => _disposer.unsubscribe(item));
+
+ for(int i = 0; i < change.addedCount; i++) {
+ var index = change.index + i,
+ row = _rows.elementAt(index);
+
+ if (row is! ObservableList) {
+ logger.severe('A non-observable row was added! '
+ 'Changes on this row will not be monitored');
+ } else {
+ _disposer.add(row.listChanges.listen((changes)
+ => _valuesChanged(index, changes)), row);
+ }
+ }
+ });
+ }
+
+ _valuesChanged(int index, List<ListChangeRecord> changes) {
+ if (!_hasObservableRows) return;
+ notifyChange(new ChartValueChangeRecord(index, changes));
+ }
+
+ @override
+ String toString() {
+ var cellDataLength = new List.filled(rows.elementAt(0).length, 0);
+ for (var i = 0; i < columns.length; i++) {
+ if (cellDataLength[i] < columns.elementAt(i).label.toString().length) {
+ cellDataLength[i] = columns.elementAt(i).label.toString().length;
+ }
+ }
+ for (var row in rows) {
+ for (var i = 0; i < row.length; i++) {
+ if (cellDataLength[i] < row.elementAt(i).toString().length) {
+ cellDataLength[i] = row.elementAt(i).toString().length;
+ }
+ }
+ }
+
+ var totalLength = 1; // 1 for the leading '|'.
+ for (var length in cellDataLength) {
+ // 3 for the leading and trailing ' ' padding and trailing '|'.
+ totalLength += length + 3;
+ }
+
+ // Second pass for building the string buffer and padd each cell with space
+ // according to the difference between cell string length and max length.
+ var strBuffer = new StringBuffer();
+ strBuffer.write('-' * totalLength + '\n');
+ strBuffer.write('|');
+
+ // Process columns.
+ for (var i = 0; i < columns.length; i++) {
+ var label = columns.elementAt(i).label;
+ var lengthDiff = cellDataLength[i] - label.length;
+ strBuffer.write(' ' * lengthDiff + ' ${label} |');
+ }
+ strBuffer.write('\n' + '-' * totalLength + '\n');
+
+ // Process rows.
+ for (var row in rows) {
+ strBuffer.write('|');
+ for (var i = 0; i < row.length; i++) {
+ var data = row.elementAt(i).toString();
+ var lengthDiff = cellDataLength[i] - data.length;
+ strBuffer.write(' ' * lengthDiff + ' ${data} |');
+
+ if (i == row.length - 1) {
+ strBuffer.write('\n' + '-' * totalLength + '\n');
+ }
+ }
+ }
+ return strBuffer.toString();
+ }
+}
diff --git a/pub/charted/lib/charts/src/chart_data_waterfall_impl.dart b/pub/charted/lib/charts/src/chart_data_waterfall_impl.dart
new file mode 100644
index 0000000..2c24004
--- /dev/null
+++ b/pub/charted/lib/charts/src/chart_data_waterfall_impl.dart
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class _WaterfallChartData extends _ChartData implements WaterfallChartData {
+ Iterable<int> _baseRows;
+
+ // The last row should always be base (no shifting on y-axis). If not,
+ // calculate the sum since last base, and store them in these tables.
+ List<int> _extendedBaseRows;
+ List<List> _extendedRows;
+ bool _extended = false;
+
+ SubscriptionsDisposer _disposer = new SubscriptionsDisposer();
+
+ _WaterfallChartData(Iterable<ChartColumnSpec> columns,
+ Iterable<Iterable> rows, [Iterable<int> baseRows])
+ : super(columns, rows) {
+ // Force calling setter.
+ this.baseRows = baseRows;
+ }
+
+ @override
+ set rows(Iterable<Iterable> value) {
+ super.rows = value;
+ _extendRowsOrNot();
+ }
+
+ @override
+ Iterable<Iterable> get rows => _extended ? _extendedRows : _rows;
+
+ set baseRows(Iterable<int> value) {
+ _baseRows = value == null? new ObservableList() : value;
+
+ if (_baseRows is ObservableList) {
+ _disposer.add(
+ (_baseRows as ObservableList).listChanges.listen(_baseRowsChanged));
+ }
+
+ _extendRowsOrNot();
+ }
+
+ Iterable<int> get baseRows => _extended ? _extendedBaseRows : _baseRows;
+
+ @override
+ rowsChanged(List<ListChangeRecord> changes) {
+ super.rowsChanged(changes);
+ _extendRowsOrNot();
+ }
+
+ _baseRowsChanged(List<ListChangeRecord> changes) {
+ if (_baseRows is! ObservableList) return;
+ _extendRowsOrNot();
+ // force re-draw?
+ notifyChange(new ChartRowChangeRecord(changes));
+ }
+
+ /*
+ * If [_baseRows] does not specify last row as base row, add one more row
+ * as the running totals of measures since last base row.
+ */
+ _extendRowsOrNot() {
+ _extended = false;
+ if (_baseRows == null || _rows == null || _rows.length < 2) return;
+
+ if (!_baseRows.contains(_rows.length - 1)) {
+ _fillExtendRows();
+ _extended = true;
+ }
+ }
+
+ /*
+ * Set [_extendedRows] and [_extendedBaseRows] to include the computed
+ * last row with running totals of measures since last base row, which
+ * will be shown as base row.
+ */
+ _fillExtendRows() {
+ var start = 0;
+ _baseRows.forEach((index) => start = index > start? index : start);
+
+ var runningTotal = new List.from(_rows.elementAt(start));
+ for (int i = start + 1; i < _rows.length; i++) {
+ for (int j = 1 /* skip label */; j < _rows.elementAt(i).length; j++) {
+ runningTotal[j] += _rows.elementAt(i).elementAt(j);
+ }
+ }
+ runningTotal[0] = 'Total';
+
+ _extendedRows = new List.from(_rows)
+ ..add(runningTotal);
+ _extendedBaseRows = new List.from(_baseRows)
+ ..add(_extendedRows.length - 1);
+ }
+}
diff --git a/pub/charted/lib/charts/src/chart_events_impl.dart b/pub/charted/lib/charts/src/chart_events_impl.dart
new file mode 100644
index 0000000..2ca79c3
--- /dev/null
+++ b/pub/charted/lib/charts/src/chart_events_impl.dart
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class _ChartEvent implements ChartEvent {
+ @override
+ final _ChartArea area;
+
+ @override
+ final ChartSeries series;
+
+ @override
+ final MouseEvent source;
+
+ @override
+ final int column;
+
+ @override
+ final int row;
+
+ @override
+ final num value;
+
+ @override
+ num scaledX;
+
+ @override
+ num scaledY;
+
+ @override
+ num chartX;
+
+ @override
+ num chartY;
+
+ _ChartEvent(this.source, this.area,
+ [this.series, this.row, this.column, this.value]) {
+ var host = area.host;
+ var hostRect = host.getBoundingClientRect();
+ chartX = source.client.x - hostRect.left - _ChartArea.MARGIN;
+ chartY = source.client.y - hostRect.top - _ChartArea.MARGIN;
+ }
+}
diff --git a/pub/charted/lib/charts/src/chart_legend_impl.dart b/pub/charted/lib/charts/src/chart_legend_impl.dart
new file mode 100644
index 0000000..00a4420
--- /dev/null
+++ b/pub/charted/lib/charts/src/chart_legend_impl.dart
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class _ChartLegend implements ChartLegend {
+ final Element host;
+ final int _maxItems;
+ String _title;
+ SelectionScope _scope;
+ Selection _selected;
+
+ _ChartLegend(Element this.host, int this._maxItems, String this._title) {
+ assert(host != null);
+ }
+
+ /**
+ * Sets the title of the legend, if the legend is already drawn, updates the
+ * title on the legend as well.
+ */
+ void set title(String title) {
+ _title = title;
+ if (_scope == null) return;
+ _updateTitle();
+ }
+
+ String get title => _title;
+
+ /** Updates the title of the legend. */
+ void _updateTitle() {
+ if (_title.isNotEmpty) {
+ if (_selected.select('.legend-title').length == 0) {
+ _selected.select('.legend-title');
+ _selected.append('div')
+ ..classed('legend-title')
+ ..text(_title);
+ } else {
+ _selected.select('.legend-title').text(_title);
+ }
+ }
+ }
+
+ /** Updates the legend base on a new list of ChartLegendItems. */
+ update(Iterable<ChartLegendItem> items, ChartArea chart) {
+ assert(items != null);
+
+ if (_scope == null) {
+ _scope = new SelectionScope.element(host);
+ _selected = _scope.selectElements([host]);
+ }
+
+ _updateTitle();
+
+ _createLegendItems(_selected, 'legend',
+ (_maxItems > 0) ? items.take(_maxItems) : items);
+
+ // Add more item label if there's more items than the max display items.
+ if ((_maxItems > 0) && (_maxItems < items.length)) {
+ _selected.select('.legend-more').remove();
+ _selected.append('div')
+ ..on('mouseover', (d, i, e) => _displayMoreItem(items.skip(_maxItems)))
+ ..on('mouseleave', (d, i, e) => _hideMoreItem())
+ ..text('${items.length - _maxItems} more...')
+ ..classed('legend-more');
+ }
+ }
+
+ /** Hides extra legend items. */
+ void _hideMoreItem() {
+ var tooltip = _selected.select('.legend-more-tooltip');
+ tooltip.style('opacity', '0');
+ }
+
+ /** Display more legend items. */
+ void _displayMoreItem(Iterable<ChartLegendItem> items) {
+ var tooltip = _selected.select('.legend-more-tooltip');
+ if (tooltip.isEmpty) {
+ tooltip = _selected.select('.legend-more').append('div')
+ ..classed('legend-more-tooltip');
+ }
+ tooltip.style('opacity', '1');
+
+ _createLegendItems(tooltip, 'legend-more', items);
+ }
+
+ /**
+ * Creates a list of legend items base on the label of the [items], appending
+ * legend item classes and prefix them with [classPrefix] and attatch the
+ * items to [host].
+ */
+ void _createLegendItems(Selection host, String classPrefix,
+ Iterable<ChartLegendItem> items) {
+ var rows = host.selectAll('.${classPrefix}-row').data(items);
+ rows.enter.appendWithCallback((d, i, e) {
+ Element row = new Element.tag('div')
+ ..append(new Element.tag('div')
+ ..className = '${classPrefix}-color')
+ ..append(new Element.tag('div')
+ ..className = '${classPrefix}-column');
+ return row;
+ });
+
+ rows.classed('${classPrefix}-row');
+ rows.exit.remove();
+
+ // This is needed to update legend colors when a column is removed or
+ // inserted not at the tail of the list of ChartLegendItem.
+ _selected.selectAll('.${classPrefix}-color').data(items).styleWithCallback(
+ 'background-color', (d, i, c) => d.color);
+
+ _selected.selectAll('.${classPrefix}-column').data(items)
+ ..textWithCallback((d, i, e) => d.label);
+ }
+}
diff --git a/pub/charted/lib/charts/src/chart_series_impl.dart b/pub/charted/lib/charts/src/chart_series_impl.dart
new file mode 100644
index 0000000..9103a44
--- /dev/null
+++ b/pub/charted/lib/charts/src/chart_series_impl.dart
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class _ChartSeries extends ChangeNotifier implements ChartSeries {
+ final String name;
+
+ Iterable<String> _measureAxisIds;
+ Iterable<int> _measures;
+ ChartRenderer _renderer;
+
+ SubscriptionsDisposer _disposer = new SubscriptionsDisposer();
+
+ _ChartSeries(this.name, Iterable<int> measures, this._renderer,
+ Iterable<String> measureAxisIds) {
+ this.measures = measures;
+ this.measureAxisIds = measureAxisIds;
+ }
+
+ set renderer(ChartRenderer value) {
+ if (value != null && value == _renderer) return;
+ _renderer.dispose();
+ _renderer = value;
+ notifyChange(new ChartSeriesChangeRecord(this));
+ }
+
+ ChartRenderer get renderer => _renderer;
+
+ set measures(Iterable<int> value) {
+ _measures = value;
+
+ if (_measures is ObservableList) {
+ _disposer.add(
+ (_measures as ObservableList).listChanges.listen(_measuresChanged));
+ }
+ }
+
+ Iterable<int> get measures => _measures;
+
+ set measureAxisIds(Iterable<String> value) => _measureAxisIds = value;
+ Iterable<String> get measureAxisIds => _measureAxisIds;
+
+ _measuresChanged(_) {
+ if (_measures is! ObservableList) return;
+ notifyChange(new ChartSeriesChangeRecord(this));
+ }
+}
diff --git a/pub/charted/lib/charts/themes/quantum_theme.dart b/pub/charted/lib/charts/themes/quantum_theme.dart
new file mode 100644
index 0000000..42901d9
--- /dev/null
+++ b/pub/charted/lib/charts/themes/quantum_theme.dart
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+class QuantumChartTheme extends ChartTheme {
+ static const List<List<String>> COLORS = const[
+ const [ '#C5D9FB', '#4184F3', '#2955C5' ],
+ const [ '#F3C6C2', '#DB4437', '#DB4437' ],
+ const [ '#FBE7B1', '#F4B400', '#EF9200' ],
+ const [ '#B6E0CC', '#0F9D58', '#0A7F42' ],
+ const [ '#E0BDE6', '#AA46BB', '#691A99' ],
+ const [ '#B1EAF1', '#00ABC0', '#00828E' ],
+ const [ '#FFCBBB', '#FF6F42', '#E54918' ],
+ const [ '#EFF3C2', '#9D9C23', '#817616' ],
+ const [ '#C4C9E8', '#5B6ABF', '#3848AA' ],
+ const [ '#F7BACF', '#EF6191', '#E81D62' ],
+ const [ '#B1DEDA', '#00786A', '#004C3F' ],
+ const [ '#F38EB0', '#C1175A', '#870D4E' ],
+ ];
+
+ static const List<List<String>> COLORS_ASSIST = const[
+ const [ '#C5D9FB', '#4184F3', '#2955C5' ],
+ const [ '#F3C6C2', '#DB4437', '#DB4437' ],
+ const [ '#FBE7B1', '#F4B400', '#EF9200' ],
+ const [ '#B6E0CC', '#0F9D58', '#0A7F42' ],
+ const [ '#E0BDE6', '#AA46BB', '#691A99' ],
+ const [ '#B1EAF1', '#00ABC0', '#00828E' ],
+ const [ '#FFCBBB', '#FF6F42', '#E54918' ],
+ const [ '#EFF3C2', '#9D9C23', '#817616' ]
+ ];
+
+ final OrdinalScale _scale = new OrdinalScale()..range = COLORS;
+
+ /* Implementation of ChartTheme */
+ String getColorForKey(key, [int state = ChartTheme.STATE_NORMAL]) {
+ var result = _scale.apply(key);
+ return (result is List && result.length > state) ?
+ result.elementAt(state) : result;
+ }
+
+ ChartAxisTheme get measureAxisTheme =>
+ const _QuantumChartAxisTheme(ChartAxisTheme.FILL_RENDER_AREA, 5);
+ ChartAxisTheme get dimensionAxisTheme =>
+ const _QuantumChartAxisTheme(0, 5);
+
+ /* Padding for charts */
+ double outerPadding = 0.1;
+ double bandInnerPadding = 0.35;
+ double bandOuterPadding = 0.175;
+
+ /* Tick sizes */
+ int dimensionTickSize = 0;
+ int measureTickSize = -1000;
+ int dimensionTickPadding = 6;
+ int measureTickPadding = 6;
+}
+
+class _QuantumChartAxisTheme implements ChartAxisTheme {
+ final axisOuterPadding = 0.1;
+ final axisBandInnerPadding = 0.35;
+ final axisBandOuterPadding = 0.175;
+ final axisTickPadding = 6;
+ final axisTickSize;
+ final axisTickCount;
+ final axisAutoResize = true;
+ final verticalAxisWidth = 50;
+ final horizontalAxisHeight = 50;
+ const _QuantumChartAxisTheme(this.axisTickSize, this.axisTickCount);
+}
diff --git a/pub/charted/lib/charts/transformers/aggregation.dart b/pub/charted/lib/charts/transformers/aggregation.dart
new file mode 100644
index 0000000..3a8978f
--- /dev/null
+++ b/pub/charted/lib/charts/transformers/aggregation.dart
@@ -0,0 +1,728 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Function callback to filter items in the input
+ */
+typedef bool AggregationFilterFunc(var item);
+
+typedef dynamic FieldAccessor(dynamic item, dynamic key);
+
+
+// TODO(midoringo, prsd): Consider splitting each aggregation type into its own
+// strategy object for readability, maintainability, and scalability.
+/**
+ * Given list of items, dimensions and facts, compute
+ * aggregates (COUNT, SUM, MIN, MAX) for facts at each dimension level.
+ */
+class AggregationModel {
+
+ // Number of aggregations we collect on each fact
+ int _aggregationTypesCount = 0;
+
+ // Currently supported list of aggregations.
+ static final List<String> supportedAggregationTypes =
+ ['sum', 'min', 'max', 'valid'];
+
+ // Computed aggregation types.
+ List<String> computedAggregationTypes;
+
+ // Offsets of aggregations that are computed once per fact per dimension
+ // If an offset is null, it will not be computed
+ int _offsetSum;
+ int _offsetMin;
+ int _offsetMax;
+ int _offsetCnt;
+
+ // Offset of aggregations that one computed once per dimension
+ int _offsetFilteredCount;
+ int _offsetSortedIndex;
+
+ // Number of bits we can use in an integer without making it medium int
+ static const int SMI_BITS = 30;
+
+ // Computed aggregations
+ static const int AGGREGATIONS_BUFFER_LENGTH = 1024 * 1024;
+ Float64List _aggregations;
+
+ // Cache of fact values
+ Float64List _factsCache;
+
+ // Cache of enumerated dimensions
+ List<int> _dimEnumCache;
+
+ // Sorted list of indices (for data in _rows/_dimEnumCache/_factsCache)
+ List<int> _sorted;
+
+ // Enumeration map for dimension values
+ List<Map<dynamic, int>> _dimToIntMap;
+
+ // Sort orders for dimension values
+ List<List<int>> _dimSortOrders;
+
+ // Input
+ List _rows;
+ List _dimFields;
+ List _factFields;
+
+ // When groupBy is called, this value represents the
+ // common prefix of the old and new dimensions list.
+ int _dimPrefixLength = 0;
+
+ // Dimensions mapped to computed aggregates
+ Map<String, int> _dimToAggrMap;
+
+ // null when no filter was applied.
+ // Otherwise, store a bitmap indicating if an item was included.
+ List<int> _filterResults;
+
+ // Cache of entities created for the facts on this aggregation view.
+ Map<String, AggregationItem> _entityCache;
+
+ // List of field names that aggregation items will have.
+ List<String> _itemFieldNamesCache;
+
+ // Walk through the map, by splitting key at '.'
+ final bool walkThroughMap;
+
+ // Map of fieldName to comparator function.
+ final Map<String, Function> comparators;
+
+ // Timing operations
+ static final Logger _logger = new Logger('aggregations');
+ Stopwatch _timeItWatch;
+ String _timeItName;
+
+ FieldAccessor dimensionAccessor;
+ FieldAccessor factsAccessor;
+
+ /**
+ * Create a new [AggregationModel] from a [collection] of items,
+ * list of [dimensions] on which the items are grouped and a list of [facts]
+ * on which aggregations are computed.
+ */
+ AggregationModel(List collection, List dimensions,
+ List facts,
+ { List<String> aggregationTypes,
+ this.walkThroughMap: false,
+ this.comparators,
+ this.dimensionAccessor,
+ this.factsAccessor}) {
+ _init(collection, dimensions, facts, aggregationTypes);
+ }
+
+ void _timeItStart(String name) {
+ _timeItName = name;
+ _timeItWatch = new Stopwatch();
+ _timeItWatch.start();
+ }
+
+ void _timeItEnd() {
+ _timeItWatch.stop();
+ _logger.info('[aggregations/$_timeItName] '
+ '${_timeItWatch.elapsed.inMilliseconds}ms/${_rows.length}r');
+ }
+
+ List get factFields => _factFields;
+ List get dimensionFields => _dimFields;
+
+ /**
+ * Initialize the view
+ */
+ void _init(List collection, List dimensions,
+ List facts, List<String> aggregationTypes) {
+ if (collection == null) {
+ throw new ArgumentError('Data cannot be empty or null');
+ }
+
+ if (facts == null || facts.isEmpty) {
+ throw new ArgumentError('Facts cannot be empty or null');
+ }
+
+ if (dimensions == null) {
+ dimensions = [];
+ }
+
+ if (dimensionAccessor == null) {
+ dimensionAccessor = _fetch;
+ }
+
+ if (factsAccessor == null) {
+ factsAccessor = _fetch;
+ }
+
+ if (aggregationTypes != null) {
+ Iterable unknownAggregationTypes =
+ aggregationTypes.where((e) => !supportedAggregationTypes.contains(e));
+ if (unknownAggregationTypes.length != 0) {
+ throw new ArgumentError(
+ 'Unknown aggregation types: ${unknownAggregationTypes.join(', ')}');
+ }
+ } else {
+ aggregationTypes = ['sum'];
+ }
+
+ // Always adding 'count' for correct computation of average and count.
+ if (!aggregationTypes.contains('valid')) {
+ aggregationTypes.add('valid');
+ }
+
+ _rows = collection;
+ _dimFields = new List.from(dimensions, growable: false);
+ _factFields = new List.from(facts, growable: false);
+ _entityCache = new Map<String, AggregationItem>();
+
+ _createBuffers();
+
+ _aggregationTypesCount = aggregationTypes.length;
+ for (int i = 0; i < _aggregationTypesCount; i++) {
+ switch(aggregationTypes[i]) {
+ case 'sum':
+ _offsetSum = i;
+ break;
+ case 'min':
+ _offsetMin = i;
+ break;
+ case 'max':
+ _offsetMax = i;
+ break;
+ case 'valid':
+ _offsetCnt = i;
+ break;
+ }
+ }
+ computedAggregationTypes = new List.from(aggregationTypes, growable: false);
+
+ // Preprocess the data
+ _preprocess();
+ }
+
+ /**
+ * Re-calculate aggregations based on new dimensions.
+ */
+ void groupBy(List dimensions, [AggregationFilterFunc filter = null]) {
+ if (dimensions == null) {
+ dimensions = [];
+ }
+
+ List savedDimFields = _dimFields;
+ _dimFields = new List.from(dimensions, growable: false);
+
+ _dimPrefixLength = 0;
+ while (_dimPrefixLength < _dimFields.length &&
+ _dimPrefixLength < savedDimFields.length &&
+ savedDimFields[_dimPrefixLength] == _dimFields[_dimPrefixLength]) {
+ ++_dimPrefixLength;
+ }
+
+ _createBuffers();
+ _preprocess(groupBy:true);
+
+ // For groupBy, compute immediately.
+ compute(filter);
+
+ // Ensure that cache represents updated dimensions
+ _updateCachedEntities();
+ }
+
+ /**
+ * Create buffers.
+ * This method is called when the object is being created and when
+ * a groupBy is called to change the dimensions on which
+ * aggregations are computed.
+ */
+ void _createBuffers() {
+ // Create both when object is created and groupBy is called
+ _dimEnumCache = new Int32List(_dimFields.length * _rows.length);
+
+ // Create only when the object is created
+ if (_factsCache == null) {
+ _factsCache = new Float64List((_factFields.length + 1) * _rows.length);
+ }
+
+ // Create only when the object is created
+ if (_filterResults == null) {
+ _filterResults = new List<int>((_rows.length) ~/ SMI_BITS + 1);
+ }
+
+ // Create both when object is created and groupBy is called
+ // Reuse dimension enumerations if possible.
+ var oldDimToInt = _dimToIntMap;
+ _dimToIntMap = new List<Map<dynamic, int>>.generate(_dimFields.length,
+ (i) => i < _dimPrefixLength ? oldDimToInt[i] : new Map<dynamic, int>());
+ }
+
+ /**
+ * Check cache entries
+ * When data is grouped by a new dimensions, entities that were
+ * created prior to the groupBy should be cleared and removed from cache
+ * if they aren't valid anymore.
+ * Update the entities that are valid after the groupBy.
+ */
+ void _updateCachedEntities() {
+ List keys = new List.from(_entityCache.keys, growable: false);
+ keys.forEach((key) {
+ _AggregationItemImpl entity = _entityCache[key];
+ if (entity == null) {
+ _entityCache.remove(key);
+ } else if (entity != null && entity.isValid) {
+ if (key.split(':').length <= _dimPrefixLength) {
+ entity.update();
+ } else {
+ _entityCache.remove(key);
+ entity.clear();
+ }
+ }
+ });
+ }
+
+ final Map<String, List> _parsedKeys = {};
+ /**
+ * Get value from a map-like object
+ */
+ dynamic _fetch(var item, String key) {
+ if (walkThroughMap && key.contains('.')) {
+ return walk(item, key, _parsedKeys);
+ } else {
+ return item[key];
+ }
+ }
+
+ /*
+ * Preprocess Data
+ * - Enumerate dimension values
+ * - Create sort orders for dimension values
+ * - Cache facts in lists
+ */
+ void _preprocess({bool groupBy: false}) {
+
+ _timeItStart('preprocess');
+
+ // Enumerate dimensions...
+ // Cache dimensions and facts.
+
+ List<int> dimensionValCount =
+ new List<int>.generate(_dimFields.length, (idx) => 0);
+
+ int dimensionsCount = _dimFields.length;
+ int factsCount = _factFields.length;
+ int rowCount = _rows.length;
+
+ for (int ri = 0, factsDataOffset = 0, dimDataOffset = 0;
+ ri < rowCount; ++ri, factsDataOffset += factsCount,
+ dimDataOffset += dimensionsCount) {
+ var item = _rows[ri];
+
+ // Cache the fact values in the big buffer, but only
+ // when we are initializing (not when a groupBy was called
+ // after initialization)
+ if (!groupBy) {
+ for (int fi = 0; fi < factsCount; fi++) {
+ var value = factsAccessor(item,_factFields[fi]);
+ _factsCache[factsDataOffset + fi] =
+ (value == null) ? double.NAN : value.toDouble();
+ }
+ }
+
+ // Enumerate the dimension values and cache enumerated rows
+ for (int di = 0; di < dimensionsCount; di++) {
+ var dimensionVal = dimensionAccessor(item, _dimFields[di]);
+ int dimensionValEnum = _dimToIntMap[di][dimensionVal];
+ if (dimensionValEnum == null) {
+ _dimToIntMap[di][dimensionVal] = dimensionValCount[di];
+ dimensionValEnum = dimensionValCount[di]++;
+ }
+ _dimEnumCache[dimDataOffset + di] = dimensionValEnum;
+ }
+ }
+
+ // Sort all dimensions internally
+ // The resulting arrays would be used to sort the entire data
+
+ List oldSortOrders = _dimSortOrders;
+ _dimSortOrders = new List.generate(dimensionsCount, (i) {
+ if (groupBy && i < _dimPrefixLength) {
+ return oldSortOrders[i];
+ }
+
+ List dimensionVals = new List.from(_dimToIntMap[i].keys);
+ List retval = new List(_dimToIntMap[i].length);
+
+ // When a comparator is not specified, our implementation of the
+ // comparator tries to gracefully handle null values.
+ dimensionVals.sort(
+ comparators != null && comparators.containsKey(_dimFields[i]) ?
+ comparators[_dimFields[i]] : _defaultDimComparator);
+
+ for (int si = 0; si < retval.length; ++si) {
+ retval[_dimToIntMap[i][dimensionVals[si]]] = si;
+ }
+ return retval;
+ }, growable: false);
+
+ // Create a list of sorted indices - only if we are not having a full
+ // overlap of dimensionFields.
+ if (!groupBy || _dimPrefixLength != _dimFields.length) {
+ _sorted = new List<int>.generate(_rows.length, (i) => i, growable: false);
+ _sorted.sort(_comparator);
+ }
+
+ // Pre-compute frequently used values
+ _offsetSortedIndex = factsCount * _aggregationTypesCount;
+ _offsetFilteredCount = factsCount * _aggregationTypesCount + 1;
+
+ _timeItEnd();
+ }
+
+ // Ensures that null dimension values don't cause an issue with sorting
+ _defaultDimComparator(Comparable left, Comparable right) =>
+ (left == null && right == null) ? 0 :
+ (left == null) ? -1 :
+ (right == null) ? 1 : left.compareTo(right);
+
+ /*
+ * Given item indices in rows, compare them based
+ * on the sort orders created while preprocessing data.
+ */
+ _comparator(int one, int two) {
+ if (one == two) {
+ return 0;
+ }
+
+ int offsetOne = _dimFields.length * one;
+ int offsetTwo = _dimFields.length * two;
+
+ for (int i = 0; i < _dimFields.length; ++i) {
+ int diff = _dimSortOrders[i][_dimEnumCache[offsetOne + i]] -
+ _dimSortOrders[i][_dimEnumCache[offsetTwo + i]];
+ if (diff != 0) {
+ return diff;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Compute aggregations
+ * If [filter] is not null, it would be used to filter out items that
+ * should not be included in the aggregates.
+ */
+ void compute([AggregationFilterFunc filter = null]) {
+ _timeItStart('compute');
+
+ _dimToAggrMap = new Map<String, int>();
+ _aggregations = new Float64List(AGGREGATIONS_BUFFER_LENGTH);
+ _filterResults = filter == null ?
+ null : new List<int>.filled((_rows.length ~/ SMI_BITS) + 1, 0);
+
+ int rowCount = _rows.length;
+ int dimensionCount = _dimFields.length;
+ int factsCount = _factFields.length;
+
+ // Saves current dimension value to which we are aggregating
+ // Values of dimensions are in even indices (starting at 0) and
+ // location of respective dimension in buffer is in odd indices.
+ List<int> currentDim = new List<int>(dimensionCount * 2);
+ bool reset = true;
+ bool isNewDimension = false;
+ int aggregationSizePerDim = factsCount * _aggregationTypesCount;
+
+ // Reserve the 0th position for top-level aggregations.
+ int currentBufferPos = (factsCount * _aggregationTypesCount + 2);
+ _dimToAggrMap[''] = 0;
+ _aggregations[_offsetSortedIndex] = 0.0;
+
+
+ for (int ri = 0, index = 0, dimensionDataOffset = 0, factsDataOffset = 0;
+ ri < rowCount; ++ri, reset = false) {
+
+ // If filter is not null, check if this row must be included in
+ // the aggregations and mark it accordingly.
+ index = _sorted[ri];
+ if (filter != null) {
+ if (!filter(_rows[index])) {
+ continue;
+ } else {
+ _filterResults[ri ~/ SMI_BITS] |= (1 << (ri % SMI_BITS));
+ }
+ }
+
+ dimensionDataOffset = index * dimensionCount;
+ factsDataOffset = index * factsCount;
+
+ // Update top-level aggregations.
+ _updateAggregationsAt(0, factsDataOffset, ri == 0 ? true : false);
+
+ // See which dimensions get effected by this row.
+ // di => the index of the dimension
+ // ci => index of the cached value in [currentDim]
+ for (int di = 0, ci = 0; di < dimensionCount; ++di, ci += 2) {
+ // If a dimension value changed, then all dimensions that are lower
+ // in the heirarchy change too.
+ if (reset ||
+ currentDim[ci] != _dimEnumCache[dimensionDataOffset + di]) {
+ currentDim[ci] = _dimEnumCache[dimensionDataOffset + di];
+ currentDim[ci + 1] = currentBufferPos;
+
+ // Save location to aggregations position in the buffer
+ _dimToAggrMap[new List.generate(di + 1,
+ (i) => currentDim[2 * i]).join(':')] = currentBufferPos;
+
+ // Store items start position
+ _aggregations[currentBufferPos + _offsetSortedIndex] = ri.toDouble();
+
+ // After the aggregated values, we save the filtered count,
+ // index of the first item (in sorted)
+ currentBufferPos += (aggregationSizePerDim + 2);
+ reset = true;
+ isNewDimension = true;
+ }
+
+ _updateAggregationsAt(currentDim[ci + 1],
+ factsDataOffset, isNewDimension);
+ isNewDimension = false;
+ }
+ }
+
+ _timeItEnd();
+ }
+
+ /**
+ * Helper function that does the actual aggregations.
+ * This function is called once per row per dimension.
+ */
+ _updateAggregationsAt(int aggrDataOffset,
+ int factsDataOffset, bool isNewDimension) {
+ // Update count.
+ _aggregations[aggrDataOffset + _offsetFilteredCount] += 1;
+
+ // Update aggregation for each of the facts.
+ for (int fi = 0, bufferFactOffset = aggrDataOffset;
+ fi < _factFields.length;
+ bufferFactOffset += _aggregationTypesCount, ++fi) {
+
+ double factValue = _factsCache[factsDataOffset + fi];
+ if (factValue.isNaN) {
+ continue;
+ }
+
+ // Sum
+ if (_offsetSum != null) {
+ _aggregations[bufferFactOffset + _offsetSum] += factValue;
+ }
+
+ // Min
+ if (_offsetMin != null && (isNewDimension || factValue <
+ _aggregations[bufferFactOffset + _offsetMin])) {
+ _aggregations[bufferFactOffset + _offsetMin] = factValue;
+ }
+
+ // Max
+ if (_offsetMax != null && (isNewDimension || factValue >
+ _aggregations[bufferFactOffset + _offsetMax])) {
+ _aggregations[bufferFactOffset + _offsetMax] = factValue;
+ }
+
+ // Count
+ if (_offsetCnt != null) {
+ _aggregations[bufferFactOffset + _offsetCnt]++;
+ }
+ }
+ }
+
+ /*
+ * TODO(prsd):
+ * 1. Implementation of updates and posting updates to entities.
+ * patchEntity and addToEntity must add listeners on AggregationItems
+ * and any changes must be propagated to entities.
+ * 2. Updates (add/remove/update) should do their best to update the
+ * aggregations and then maybe do a delayed recomputation (sort etc;)
+ */
+
+ /**
+ * Update an item.
+ * If aggregates were already computed, they are updated to reflect the
+ * new value and any observers are notified.
+ */
+ void updateItem(dynamic item, String field) {
+ throw new UnimplementedError();
+ }
+
+ /**
+ * Add a new item.
+ * If aggregates were already computed, they are updated to reflect
+ * values on the new item.
+ */
+ void addItem(dynamic item) {
+ throw new UnimplementedError();
+ }
+
+ /**
+ * Remove an existing item.
+ * If aggregates were already computed, they are updated to reflect
+ * facts on the removed item.
+ */
+ void removeItem(dynamic item) {
+ throw new UnimplementedError();
+ }
+
+ /**
+ * Return an [AggregationItem] that represents facts for dimension
+ * represented by [dimension] Only one instance of an entity is created
+ * per dimension (even if this function is called multiple times)
+ *
+ * Callers of this method can observe the returned entity for updates to
+ * aggregations caused by changes to filter or done through add, remove
+ * or modify of items in the collection.
+ */
+ AggregationItem facts(List dimension) {
+ List<int> enumeratedList = new List<int>();
+ for (int i = 0; i < dimension.length; ++i) {
+ enumeratedList.add(_dimToIntMap[i][dimension[i]]);
+ }
+
+ String key = enumeratedList.join(':');
+ AggregationItem item = _entityCache[key];
+
+ if (item == null && _dimToAggrMap.containsKey(key)) {
+ item = new _AggregationItemImpl(this, dimension, key);
+ _entityCache[key] = item;
+ }
+
+ return item;
+ }
+
+ /**
+ * Return a list of values that are present for a dimension field.
+ */
+ List valuesForDimension(dynamic dimensionFieldName) {
+ int di = _dimFields.indexOf(dimensionFieldName);
+ if (di < 0) {
+ return null;
+ }
+ List values = new List.from(_dimToIntMap[di].keys);
+ values.sort(
+ comparators != null && comparators.containsKey(dimensionFieldName) ?
+ comparators[dimensionFieldName] : _defaultDimComparator);
+ return values;
+ }
+}
+
+/**
+ * Parse a path for nested map-like objects.
+ * Caches the parsed key in the passed map.
+ *
+ * Takes map keys of the format:
+ * "list(key=val;val=m).another(state=good).state"
+ * and outputs:
+ * ["list", {"key": "val", "val": "m"},
+ * "another", {"state": "good"}, "state"]
+ */
+List _parseKey(String key, Map parsedKeysCache) {
+ List parts = parsedKeysCache == null ? null : parsedKeysCache[key];
+ if (parts == null && key != null) {
+ parts = new List();
+ if (key.contains(').')) {
+ int start = 0;
+ int cursor = 0;
+ bool inParams = false;
+ List matchKeyVals;
+ Map listMatchingMap = {};
+
+ while (cursor < key.length) {
+ if (!inParams) {
+ cursor = key.indexOf('(', start);
+ if (cursor == -1) {
+ parts.addAll(key.substring(start).split('.'));
+ break;
+ }
+ parts.addAll(key.substring(start, cursor).split('.'));
+ cursor++;
+ start = cursor;
+ inParams = true;
+ } else {
+ cursor = key.indexOf(')', start);
+ if (cursor == -1) {
+ throw new ArgumentError('Invalid field name: $key');
+ }
+ listMatchingMap.clear();
+ matchKeyVals = key.substring(start, cursor).split(';');
+ matchKeyVals.forEach((value) {
+ List keyval = value.split('=');
+ if (keyval.length != 2) {
+ throw new ArgumentError('Invalid field name: ${key}');
+ }
+ listMatchingMap[keyval[0]] = keyval[1];
+ });
+ parts.add(listMatchingMap);
+ cursor += 2;
+ start = cursor;
+ inParams = false;
+ }
+ }
+ } else {
+ parts = key.split('.');
+ }
+ if (parsedKeysCache != null) {
+ parsedKeysCache[key] = parts;
+ }
+ }
+
+ return parts;
+}
+
+/**
+ * Walk a map-like structure that could have list in the path.
+ *
+ * Example:
+ * Map testMap = {
+ * "first": "firstval",
+ * "list": [
+ * { "val": "m",
+ * "key": "val",
+ * "another": [
+ * { 'state': 'good' },
+ * { 'state': 'bad' }
+ * ]
+ * },
+ * { "val": "m", "key": "invalid" },
+ * { "val": "o" }
+ * ]
+ * };
+ *
+ * For the above map:
+ * walk(testMap, "list(key=val;val=m).another(state=good).state");
+ * outputs:
+ * good
+ */
+dynamic walk(initial, String key, Map parsedKeyCache) {
+ List parts = _parseKey(key, parsedKeyCache);
+ return parts.fold(initial, (current, part) {
+ if (current == null) {
+ return null;
+ } else if (current is List && part is Map) {
+ for (int i = 0; i < current.length; i++) {
+ bool match = true;
+ part.forEach((key, val) {
+ if ((key.contains('.') &&
+ walk(part, key, parsedKeyCache).toString() != val) ||
+ part[key] != val) {
+ match = false;
+ }
+ });
+ if (match) {
+ return current[i];
+ }
+ }
+ } else {
+ return current[part];
+ }
+ });
+}
diff --git a/pub/charted/lib/charts/transformers/aggregation_item.dart b/pub/charted/lib/charts/transformers/aggregation_item.dart
new file mode 100644
index 0000000..b6c3480
--- /dev/null
+++ b/pub/charted/lib/charts/transformers/aggregation_item.dart
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * AggregationItem is created by [AggregationModel] to make access to facts
+ * observable. Users must use AggregationItem.isValid before trying to access
+ * the aggregations.
+ */
+abstract class AggregationItem extends ChangeNotifier {
+ /**
+ * List of dimension fields in effect
+ */
+ List<String> dimensions;
+
+ /**
+ * Check if this entity is valid.
+ * Currently the only case where an entity becomes invalid
+ * is when a groupBy is called on the model.
+ */
+ bool get isValid;
+
+ /**
+ * Fetch the fact from AggregationModel and return it
+ * Currently takes keys in the form of "sum(spend)", where sum is
+ * the aggregation type and spend is fact's field name.
+ *
+ * Currently, "sum", "count", "min", "max", "avg", "valid" and "avgOfValid"
+ * are supported as the operators.
+ */
+
+ operator[](String key);
+
+ /**
+ * Check if we support a given key.
+ */
+ bool containsKey(String key);
+
+ /**
+ * List of valid field names for this entity.
+ * It's the combined list of accessors for individual items, items in
+ * the next dimension and all possible facts defined on the view.
+ */
+ Iterable<String> get fieldNames;
+}
+
+/*
+ * Implementation of AggregationItem
+ * Instances of _AggregationItemImpl are created only by AggregationModel
+ */
+class _AggregationItemImpl extends ChangeNotifier implements AggregationItem {
+ static final List<String> derivedAggregationTypes = ['count', 'avg'];
+
+ AggregationModel model;
+ List<String> dimensions;
+
+ String _key;
+
+ int _length;
+ int _factsOffset;
+
+ /*
+ * Currently entities are created only when they have valid aggregations
+ */
+ _AggregationItemImpl(this.model, this.dimensions, this._key) {
+ if (model == null) {
+ throw new ArgumentError('Model cannot be null');
+ }
+ if (_key == null) {
+ _key = '';
+ }
+
+ // facts + list of items + list of children (drilldown)
+ _length = model._aggregationTypesCount * model._factFields.length + 2;
+ _factsOffset = model._dimToAggrMap[_key];
+ }
+
+ /**
+ * _dimToAggrMap got updated on the model, update ourselves accordingly
+ */
+ void update() {
+ _factsOffset = model._dimToAggrMap[_key];
+ }
+
+ /*
+ * Mark this entity as invalid.
+ */
+ void clear() {
+ _factsOffset = null;
+ }
+
+ bool get isValid => _factsOffset != null;
+
+ dynamic operator[](String key) {
+ if (!isValid) {
+ throw new StateError('Entity is not valid anymore');
+ }
+
+ int argPos = key.indexOf('(');
+ if (argPos == -1) {
+ return _nonAggregationMember(key);
+ }
+
+ String aggrFunc = key.substring(0, argPos);
+ int aggrFuncIndex = model.computedAggregationTypes.indexOf(aggrFunc);
+ if (aggrFuncIndex == -1 && !derivedAggregationTypes.contains(aggrFunc)) {
+ throw new ArgumentError('Unknown aggregation method: ${aggrFunc}');
+ }
+
+ String factName = key.substring(argPos + 1, key.lastIndexOf(')'));
+ int factIndex = model._factFields.indexOf(factName);
+
+ // Try parsing int if every element in factFields is int.
+ if (model._factFields.every((e) => e is int)) {
+ factIndex = model._factFields.indexOf(int.parse(factName,
+ onError: (e) {
+ throw new ArgumentError('Type of factFields are int but factName' +
+ 'contains non int value');
+ }));
+ }
+ if (factIndex == -1) {
+ throw new ArgumentError('Model not configured for ${factName}');
+ }
+
+ int offset = _factsOffset + factIndex * model._aggregationTypesCount;
+ // No items for the corresponding fact, so return null.
+ if (aggrFunc != 'count' && aggrFunc != 'avg' &&
+ model._aggregations[offset + model._offsetCnt].toInt() == 0) {
+ return null;
+ }
+
+ if (aggrFuncIndex != -1) {
+ return model._aggregations[offset + aggrFuncIndex];
+ } else if (aggrFunc == 'count') {
+ return model._aggregations[_factsOffset +
+ model._offsetFilteredCount].toInt();
+ } else if (aggrFunc == 'avg') {
+ return model._aggregations[offset + model._offsetSum] /
+ model._aggregations[_factsOffset + model._offsetFilteredCount].
+ toInt();
+ } else if (aggrFunc == 'avgOfValid') {
+ return model._aggregations[offset + model._offsetSum] /
+ model._aggregations[offset + model._offsetCnt].toInt();
+ }
+ return null;
+ }
+
+ dynamic _nonAggregationMember(String key) {
+ if (key == 'items') {
+ return new _AggregationItemsIterator(model, dimensions, _key);
+ }
+ if (key == 'aggregations') {
+ return _lowerAggregations();
+ }
+ return null;
+ }
+
+ List<AggregationItem> _lowerAggregations() {
+ List<AggregationItem> aggregations = new List<AggregationItem>();
+ if (dimensions.length == model._dimFields.length) {
+ return aggregations;
+ }
+
+ var lowerDimensionField = model._dimFields[dimensions.length];
+ List lowerVals = model.valuesForDimension(lowerDimensionField);
+
+ lowerVals.forEach((name) {
+ List lowerDims = new List.from(dimensions)..add(name);
+ AggregationItem entity = model.facts(lowerDims);
+ if (entity != null) {
+ aggregations.add(entity);
+ }
+ });
+
+ return aggregations;
+ }
+
+ bool containsKey(String key) => fieldNames.contains(key);
+
+ Iterable<String> get fieldNames {
+ if (!isValid) {
+ throw new StateError('Entity is not valid anymore');
+ }
+
+ if (model._itemFieldNamesCache == null) {
+ List<String> cache = new List<String>.from(['items', 'children']);
+ model._factFields.forEach((var name) {
+ AggregationModel.supportedAggregationTypes.forEach((String aggrType) {
+ cache.add('${aggrType}(${name})');
+ });
+ });
+ model._itemFieldNamesCache = cache;
+ }
+ return model._itemFieldNamesCache;
+ }
+
+ /*
+ * TODO(prsd): Implementation of [Observable]
+ */
+ Stream<List<ChangeRecord>> get changes {
+ throw new UnimplementedError();
+ }
+}
+
+class _AggregationItemsIterator implements Iterator {
+ final AggregationModel model;
+ List<String> dimensions;
+ String key;
+
+ int _current;
+ int _counter = 0;
+
+ int _start;
+ int _count;
+ int _endOfRows;
+
+ _AggregationItemsIterator(this.model, List<String> this.dimensions,
+ String this.key) {
+ int offset = model._dimToAggrMap[key];
+ if (offset != null) {
+ int factsEndOffset = offset +
+ model._factFields.length * model._aggregationTypesCount;
+ _start = model._aggregations[factsEndOffset].toInt();
+ _count = model._aggregations[factsEndOffset + 1].toInt();
+ _endOfRows = model._rows.length;
+ }
+ }
+
+ bool moveNext() {
+ if (_current == null) {
+ _current = _start;
+ } else {
+ ++_current;
+ }
+
+ if (++_counter > _count) {
+ return false;
+ }
+
+ /*
+ * If model had a filter applied, then check if _current points to a
+ * filtered-in row, else skip till we find one.
+ * Also, make sure (even if something else went wrong) we don't go
+ * beyond the number of items in the model.
+ */
+ if (this.model._filterResults != null) {
+ while ((this.model._filterResults[_current ~/ AggregationModel.SMI_BITS] &
+ (1 << _current % AggregationModel.SMI_BITS)) == 0 &&
+ _current <= _endOfRows) {
+ ++_current;
+ }
+ }
+ return (_current < _endOfRows);
+ }
+
+ get current {
+ if (_current == null || _counter > _count) {
+ return null;
+ }
+ return model._rows[model._sorted[_current]];
+ }
+}
diff --git a/pub/charted/lib/charts/transformers/aggregation_transformer.dart b/pub/charted/lib/charts/transformers/aggregation_transformer.dart
new file mode 100644
index 0000000..8199175
--- /dev/null
+++ b/pub/charted/lib/charts/transformers/aggregation_transformer.dart
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Transforms the ChartData base on the specified dimension columns and facts
+ * columns indices. The values in the facts columns will be aggregated by the
+ * tree heirarcy generated by the dimension columns. Expand and Collapse
+ * methods may be called to display different levels of aggragation.
+ *
+ * The output ChartData produced by transform() will contain only columns in the
+ * original ChartData that were specified in dimensions or facts column indices.
+ * The output column will be re-ordered first by the indices specified in the
+ * dimension column indices then by the facts column indices. The data in the
+ * cells of each row will also follow this rule.
+ */
+class AggregationTransformer extends ChangeNotifier
+ implements ChartDataTransform, ChartData {
+
+ static const String AGGREGATION_TYPE_SUM = 'sum';
+ static const String AGGREGATION_TYPE_MIN = 'min';
+ static const String AGGREGATION_TYPE_MAX = 'max';
+ static const String AGGREGATION_TYPE_VALID = 'valid';
+ final SubscriptionsDisposer _dataSubscriptions = new SubscriptionsDisposer();
+ final Set<List> _expandedSet = new Set();
+ Iterable<ChartColumnSpec> columns;
+ ObservableList<Iterable> rows = new ObservableList();
+ List<int> _dimensionColumnIndices;
+ List<int> _factsColumnIndices;
+ String _aggregationType;
+ AggregationModel _model;
+ bool _expandAllDimension = false;
+ List _selectedColumns = [];
+ FieldAccessor _indexFieldAccessor = (List row, int index) => row[index];
+ ChartData _data;
+
+ AggregationTransformer(this._dimensionColumnIndices,
+ this._factsColumnIndices,
+ [String aggregationType = AGGREGATION_TYPE_SUM]) {
+ _aggregationType = aggregationType;
+ }
+
+ /**
+ * Transforms the ChartData base on the specified dimension columns and facts
+ * columns, aggregation type and currently expanded dimensions.
+ */
+ ChartData transform(ChartData data) {
+ assert(data.columns.length > max(_dimensionColumnIndices));
+ assert(data.columns.length > max(_factsColumnIndices));
+ _data = data;
+ _registerListeners();
+ _transform();
+ return this;
+ }
+
+ /** Registers listeners if data.rows or data.columns are Observable. */
+ _registerListeners() {
+ _dataSubscriptions.dispose();
+
+ if(_data is Observable) {
+ var observable = (_data as Observable);
+ _dataSubscriptions.add(observable.changes.listen((records) {
+ _transform();
+
+ // NOTE: Currently we're only passing the first change because the chart
+ // area just draw with the updated data. When we add partial update
+ // to chart area, we'll need to handle this better.
+ notifyChange(records.first);
+ }));
+ }
+ }
+
+ /**
+ * Performs the filter transform with _data. This is called on transform and
+ * onChange if the input ChartData is Observable.
+ */
+ _transform() {
+ _model = new AggregationModel(_data.rows, _dimensionColumnIndices,
+ _factsColumnIndices, aggregationTypes: [_aggregationType],
+ dimensionAccessor: _indexFieldAccessor,
+ factsAccessor: _indexFieldAccessor);
+ _model.compute();
+
+ // If user called expandAll prior to model initiation, do it now.
+ if (_expandAllDimension) {
+ expandAll();
+ }
+
+ _selectedColumns.clear();
+ _selectedColumns.addAll(_dimensionColumnIndices);
+ _selectedColumns.addAll(_factsColumnIndices);
+
+ // Process rows.
+ rows.clear();
+ var transformedRows = <Iterable>[];
+ for (var value in _model.valuesForDimension(_dimensionColumnIndices[0])) {
+ _generateAggregatedRow(transformedRows, [value]);
+ }
+ rows.addAll(transformedRows);
+
+ // Process columns.
+ columns = new List<ChartColumnSpec>.generate(_selectedColumns.length, (index) =>
+ _data.columns.elementAt(_selectedColumns[index]));
+ }
+
+ /**
+ * Fills the aggregatedRows List with data base on the set of expanded values
+ * recursively. Currently when a dimension is expanded, rows are
+ * generated for its children but not for itself. If we want to change the
+ * logic to include itself, just move the expand check around the else clause
+ * and always write a row of data whether it's expanded or not.
+ */
+ _generateAggregatedRow(List<Iterable> aggregatedRows, List dimensionValues) {
+ var entity = _model.facts(dimensionValues);
+ var dimensionLevel = dimensionValues.length - 1;
+ var currentDimValue = dimensionValues.last;
+
+ // Dimension is not expanded at this level. Generate data rows and fill int
+ // value base on whether the column is dimension column or facts column.
+ if (!_isExpanded(dimensionValues) ||
+ dimensionValues.length == _dimensionColumnIndices.length) {
+ aggregatedRows.add(new List.generate(_selectedColumns.length, (index) {
+
+ // Dimension column.
+ if (index < _dimensionColumnIndices.length) {
+ if (index < dimensionLevel) {
+ // If column index is in a higher level, write parent value.
+ return dimensionValues[0];
+ } else if (index == dimensionLevel) {
+ // If column index is at current level, write value.
+ return dimensionValues.last;
+ } else {
+ // If column Index is in a lower level, write empty string.
+ return '';
+ }
+ } else {
+ // Write aggregated value for facts column.
+ return entity['${_aggregationType}(${_selectedColumns[index]})'];
+ }
+ }));
+ } else {
+ // Dimension is expanded, process each child dimension in the expanded
+ // dimension.
+ for (AggregationItem childAggregation in entity['aggregations']) {
+ _generateAggregatedRow(aggregatedRows, childAggregation.dimensions);
+ }
+ }
+ }
+
+ /**
+ * Expands a specific dimension and optionally expands all of its parent
+ * dimensions.
+ */
+ void expand(List dimension, [bool expandParent = true]) {
+ _expandAllDimension = false;
+ _expandedSet.add(dimension);
+ if (expandParent && dimension.length > 1) {
+ Function eq = const ListEquality().equals;
+ var dim = dimension.take(dimension.length - 1).toList();
+ if (!_expandedSet.any((e) => eq(e, dim))) {
+ expand(dim);
+ }
+ }
+ }
+
+ /**
+ * Collapses a specific dimension and optionally collapse all of its
+ * Children dimensions.
+ */
+ void collapse(List dimension, [bool collapseChildren = true]) {
+ _expandAllDimension = false;
+ if (collapseChildren) {
+ Function eq = const ListEquality().equals;
+ // Doing this because _expandedSet.where doesn't work.
+ var collapseList = [];
+ for (List dim in _expandedSet) {
+ if (eq(dim.take(dimension.length).toList(), dimension)) {
+ collapseList.add(dim);
+ }
+ }
+ _expandedSet.removeAll(collapseList);
+ } else {
+ _expandedSet.remove(dimension);
+ }
+ }
+
+ /** Expands all dimensions. */
+ void expandAll() {
+ if (_model != null) {
+ for (var value in _model.valuesForDimension(_dimensionColumnIndices[0])) {
+ _expandAll([value]);
+ }
+ _expandAllDimension = false;
+ } else {
+ _expandAllDimension = true;
+ }
+ }
+
+ void _expandAll(value) {
+ var entity = _model.facts(value);
+ _expandedSet.add(value);
+ for (AggregationItem childAggregation in entity['aggregations']) {
+ _expandAll(childAggregation.dimensions);
+ }
+ }
+
+ /** Collapses all dimensions. */
+ void collapseAll() {
+ _expandAllDimension = false;
+ _expandedSet.clear();
+ }
+
+ /** Tests if specific dimension is expanded. */
+ bool _isExpanded(List dimension) {
+ Function eq = const ListEquality().equals;
+ return _expandedSet.any((e) => eq(e, dimension));
+ }
+}
diff --git a/pub/charted/lib/charts/transformers/filter_transformer.dart b/pub/charted/lib/charts/transformers/filter_transformer.dart
new file mode 100644
index 0000000..65794be
--- /dev/null
+++ b/pub/charted/lib/charts/transformers/filter_transformer.dart
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+typedef bool FilterFunction(dynamic value);
+
+/**
+ * Transforms the ChartData base on the specified FilterDefinitions. Each row
+ * of data will be tested by passing the value at target column to the filter
+ * function. If filter function returns false, the row will be filtered out.
+ * This transformer does not modify the column part of the input ChartData.
+ */
+class FilterTransformer extends ChangeNotifier
+ implements ChartDataTransform, ChartData {
+ final SubscriptionsDisposer _dataSubscriptions = new SubscriptionsDisposer();
+ Iterable<ChartColumnSpec> columns;
+ ObservableList<Iterable> rows = new ObservableList();
+ List<FilterDefinition> filterFunctions;
+ ChartData _data;
+
+ FilterTransformer(this.filterFunctions);
+
+ /**
+ * Transforms the input data with the list of [FilterDefinition] specified in
+ * the constructor. If the rows and columns are ObservableList in the data,
+ * changes in rows and columns in input data will trigger tranform to be
+ * performed again to update the output rows and columns.
+ */
+ ChartData transform(ChartData data) {
+ _data = data;
+ _registerListeners();
+ _transform();
+ return this;
+ }
+
+ /** Registers listeners if data.rows or data.columns are Observable. */
+ _registerListeners() {
+ _dataSubscriptions.dispose();
+
+ if(_data is Observable) {
+ var observable = (_data as Observable);
+ _dataSubscriptions.add(observable.changes.listen((records) {
+ _transform();
+
+ // NOTE: Currently we're only passing the first change because the chart
+ // area just draw with the updated data. When we add partial update
+ // to chart area, we'll need to handle this better.
+ notifyChange(records.first);
+ }));
+ }
+ }
+
+ /**
+ * Performs the filter transform with _data. This is called on transform and
+ * onChange if the input ChartData is Observable.
+ */
+ _transform() {
+ columns = _data.columns;
+ rows.clear();
+
+ for (var row in _data.rows) {
+ // Add the row if each value in target column passes the filter function.
+ if (filterFunctions.every((e) =>
+ e.filterFunc(row.elementAt(e.targetColumn)))) {
+ rows.add(row);
+ }
+ }
+ }
+}
+
+class FilterDefinition {
+ final FilterFunction filterFunc;
+ final int targetColumn;
+ FilterDefinition(this.targetColumn, this.filterFunc);
+}
diff --git a/pub/charted/lib/charts/transformers/transpose_transformer.dart b/pub/charted/lib/charts/transformers/transpose_transformer.dart
new file mode 100644
index 0000000..e3acbce
--- /dev/null
+++ b/pub/charted/lib/charts/transformers/transpose_transformer.dart
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.charts;
+
+/**
+ * Transforms the ChartData by transposing the columns and rows. A label column
+ * index in the original data will need to be specified (default to 0), all
+ * values in the specified label column will be used as the label for the
+ * transformed data, all the labels in the original Chart data columns will be
+ * populated in the label column as values of that column.
+ *
+ * All values in the data except for the data in the label column must have the
+ * same type; All columns except for the lable column must have the same
+ * formatter if a formatter exist for columns.
+ */
+class TransposeTransformer extends ChangeNotifier
+ implements ChartDataTransform, ChartData {
+ final SubscriptionsDisposer _dataSubscriptions = new SubscriptionsDisposer();
+ ObservableList<ChartColumnSpec> columns = new ObservableList();
+ ObservableList<Iterable> rows = new ObservableList();
+
+ // If specified, this values of this column in the input chart data will be
+ // used as labels of the transposed column label. Defaults to first column.
+ int _labelColumn;
+ ChartData _data;
+
+ TransposeTransformer([this._labelColumn = 0]);
+
+ /**
+ * Transforms the input data with the specified label column in the
+ * constructor. If the ChartData is Observable, changes fired by the input
+ * data will trigger tranform to be performed again to update the output rows
+ * and columns.
+ */
+ ChartData transform(ChartData data) {
+ _data = data;
+ _registerListeners();
+ _transform();
+ return this;
+ }
+
+ /** Registers listeners if input ChartData is Observable. */
+ _registerListeners() {
+ _dataSubscriptions.dispose();
+
+ if(_data is Observable) {
+ var observable = (_data as Observable);
+ _dataSubscriptions.add(observable.changes.listen((records) {
+ _transform();
+
+ // NOTE: Currently we're only passing the first change because the chart
+ // area just draw with the updated data. When we add partial update
+ // to chart area, we'll need to handle this better.
+ notifyChange(records.first);
+ }));
+ }
+ }
+
+ /**
+ * Performs the transpose transform with _data. This is called on transform
+ * and on changes if ChartData is Observable.
+ */
+ _transform() {
+ // Assert all columns are of the same type and formatter, excluding the
+ // label column.
+ var type;
+ var formatter;
+ for (var i = 0; i < _data.columns.length; i++) {
+ if (i != _labelColumn) {
+ if (type == null) {
+ type = _data.columns.elementAt(i).type;
+ } else {
+ assert(type == _data.columns.elementAt(i).type);
+ }
+ if (formatter == null) {
+ formatter = _data.columns.elementAt(i).formatter;
+ } else {
+ assert(formatter == _data.columns.elementAt(i).formatter);
+ }
+ }
+ }
+
+ columns.clear();
+ rows.clear();
+ rows.addAll(new List<Iterable>.generate(_data.columns.length - 1, (i) => []));
+
+ // Populate the transposed rows' data, excluding the label column, visit
+ // each value in the original data once.
+ var columnLabels = [];
+ for (var row in _data.rows) {
+ for (var i = 0; i < row.length; i++) {
+ var columnOffset = (i < _labelColumn) ? 0 : 1;
+ if (i != _labelColumn) {
+ (rows.elementAt(i - columnOffset) as List).add(row.elementAt(i));
+ } else {
+ columnLabels.add(row.elementAt(i));
+ }
+ }
+ }
+
+ // Transpose the ColumnSpec's label into the column where the original
+ // column that is used as the new label.
+ for (var i = 0; i < rows.length; i++) {
+ var columnOffset = (i < _labelColumn) ? 0 : 1;
+ (rows.elementAt(i) as List).insert(_labelColumn,
+ _data.columns.elementAt(i + columnOffset).label);
+
+ }
+
+ // Construct new ColumnSpaces base on the label column.
+ for (var label in columnLabels) {
+ columns.add(new ChartColumnSpec(type: type, label: label,
+ formatter: formatter));
+ }
+ columns.insert(_labelColumn,
+ new ChartColumnSpec(type: ChartColumnSpec.TYPE_STRING, label:
+ _data.columns.elementAt(_labelColumn).label));
+ }
+}
diff --git a/pub/charted/lib/core/color.dart b/pub/charted/lib/core/color.dart
new file mode 100644
index 0000000..fa1547c
--- /dev/null
+++ b/pub/charted/lib/core/color.dart
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.core;
+
+class Color {
+ int r;
+ int g;
+ int b;
+ double a;
+
+ Color.fromHex(String hex, [this.a = 1.0]) {
+ var rgb = _hexToRgb(hex);
+ r = rgb[0];
+ g = rgb[1];
+ b = rgb[2];
+ }
+
+ Color.fromRgb(this.r, this.g, this.b, [this.a = 1.0]);
+
+ List _hexToRgb(String hex) {
+ List rgb = [];
+
+ if (hex.startsWith('#')) {
+ hex = hex.substring(1);
+ }
+ if (hex.length == 3) {
+ // expand to 6-digit hex code
+ hex = hex[0]*2 + hex[1]*2 + hex[2]*2;
+ }
+ rgb.add(int.parse(hex.substring(0, 2), radix: 16));
+ rgb.add(int.parse(hex.substring(2, 4), radix: 16));
+ rgb.add(int.parse(hex.substring(4, 6), radix: 16));
+ return rgb;
+ }
+
+ Color.fromColorString(String color) {
+ // TODO(midoringo): add rgba
+ if (color.startsWith('#')) {
+ var rgb = _hexToRgb(color);
+ r = rgb[0];
+ g = rgb[1];
+ b = rgb[2];
+ a = 1.0;
+ } else if (color.startsWith('rgb')) {
+ var numberRegEx = new RegExp(
+ r'rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)');
+ var match = numberRegEx.firstMatch(color);
+ r = int.parse(match.group(1));
+ g = int.parse(match.group(2));
+ b = int.parse(match.group(3));
+ a = 1.0;
+ } else {
+ r = g = b = 0;
+ a = 1.0;
+ }
+ }
+
+ String get rgbString => "rgb($r,$g,$b)";
+
+ String get rgbaString => "rgba($r,$g,$b,$a)";
+
+ String get hexString => '#' + _hexify(r) + _hexify(g) + _hexify(b);
+
+ String toString() => rgbString;
+
+ String _hexify(int v) {
+ return v < 0x10
+ ? "0" + math.max(0, v).toInt().toRadixString(16)
+ : math.min(255, v).toInt().toRadixString(16);
+ }
+
+ static bool isColorString(String input) {
+ return (input.startsWith('#') || input.startsWith('rgb'));
+ }
+
+ bool operator ==(other) {
+ if (other is! Color) return false;
+ Color color = other;
+ return r == color.r && g == color.g && b == color.b && a == color.a;
+ }
+
+ // Override hashCode using strategy from Effective Java, Chapter 3.
+ int get hashCode {
+ int result = 17;
+ result = 37 * result + r.hashCode;
+ result = 37 * result + g.hashCode;
+ result = 37 * result + b.hashCode;
+ result = 37 * result + a.hashCode;
+ return result;
+ }
+}
+
diff --git a/pub/charted/lib/core/core.dart b/pub/charted/lib/core/core.dart
new file mode 100644
index 0000000..8c1b0e9
--- /dev/null
+++ b/pub/charted/lib/core/core.dart
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+library charted.core;
+
+import "dart:html" show Element;
+import "dart:math" as math;
+import "dart:collection";
+
+part 'color.dart';
+part 'lists.dart';
+part 'math.dart';
+part 'namespace.dart';
+part 'object_factory.dart';
+part 'rect.dart';
+
+const String ORIENTATION_LEFT = 'left';
+const String ORIENTATION_RIGHT = 'right';
+const String ORIENTATION_TOP = 'top';
+const String ORIENTATION_BOTTOM = 'bottom';
+
+const String EASE_TYPE_LINEAR = 'linear';
+const String EASE_TYPE_POLY = 'poly';
+const String EASE_TYPE_QUAD = 'quad';
+const String EASE_TYPE_CUBIC = 'cubic';
+const String EASE_TYPE_SIN = 'sin';
+const String EASE_TYPE_EXP = 'exp';
+const String EASE_TYPE_CIRCLE = 'circle';
+const String EASE_TYPE_ELASTIC = 'elastic';
+const String EASE_TYPE_BACK = 'back';
+const String EASE_TYPE_BOUNCE = 'bounce';
+
+const String EASE_MODE_IN = 'in';
+const String EASE_MODE_OUT = 'out';
+const String EASE_MODE_IN_OUT = 'in-out';
+const String EASE_MODE_OUT_IN = 'out-in';
+
+/** IdentityFunction */
+identityFunction(x) => x;
+
+/** Utility method to test if [val] is null or isEmpty */
+bool isNullOrEmpty(val) => val == null || val.isEmpty;
+
+/** Class representing a pair of values */
+class Pair<T1, T2> {
+ final T1 first;
+ final T2 last;
+
+ const Pair(this.first, this.last);
+
+ bool operator==(other) =>
+ other is Pair && first == other.first && last == other.last;
+}
+
+/*
+ * TODO(prsd): Move everything below this comment to the respective
+ * sub-libraries.
+ */
+
+class ScaleUtil {
+ static List nice(List domain, Nice nice) {
+ var i0 = 0,
+ i1 = domain.length - 1,
+ x0 = domain[i0],
+ x1 = domain[i1],
+ dx;
+
+ if (x1 < x0) {
+ dx = i0;
+ i0 = i1;
+ i1 = dx;
+ dx = x0;
+ x0 = x1;
+ x1 = dx;
+ }
+
+ domain[i0] = nice.floor(x0);
+ domain[i1] = nice.ceil(x1);
+ return domain;
+ }
+
+ static Nice _niceIdentity = new Nice(identityFunction, identityFunction);
+
+ static Nice niceStep(num step) {
+ return (step > 0) ? new Nice((x) => (x / step).ceil() * step,
+ (x) => (x / step).floor() * step) : _niceIdentity;
+ }
+
+ /**
+ * Returns a Function that given a value x on the domain, returns the
+ * corrsponding value on the range on a bilinear scale.
+ *
+ * @param domain The domain of the scale.
+ * @param range The range of the scale.
+ * @param uninterpolator The uninterpolator for domain values.
+ * @param interpolator The interpolator for range values.
+ */
+ static Function bilinearScale(List domain, List range,
+ Function uninterpolator, Function interpolator) {
+ var u = uninterpolator(domain[0], domain[1]),
+ i = interpolator(range[0], range[1]);
+ return (x) => i(u(x));
+ }
+
+ /**
+ * Returns a Function that given a value x on the domain, returns the
+ * corrsponding value on the range on a polylinear scale.
+ *
+ * @param domain The domain of the scale.
+ * @param range The range of the scale.
+ * @param uninterpolator The uninterpolator for domain values.
+ * @param interpolator The interpolator for range values.
+ */
+ static Function polylinearScale(List domain, List range,
+ Function uninterpolator, Function interpolator) {
+ var u = [],
+ i = [],
+ j = 0,
+ k = math.min(domain.length, range.length) - 1;
+
+ // Handle descending domains.
+ if (domain[k] < domain[0]) {
+ domain = domain.reversed.toList();
+ range = range.reversed.toList();
+ }
+
+ while (++j <= k) {
+ u.add(uninterpolator(domain[j - 1], domain[j]));
+ i.add(interpolator(range[j - 1], range[j]));
+ }
+
+ return (x) {
+ int index = bisect(domain, x, 1, k) - 1;
+ return i[index](u[index](x));
+ };
+ }
+
+ /**
+ * Returns the insertion point i for value x such that all values in a[lo:i]
+ * will be less than x and all values in a[i:hi] will be equal to or greater
+ * than x.
+ */
+ static int bisectLeft(List a, num x, [int lo = 0, int hi = -1]) {
+ if (hi == -1) {
+ hi = a.length;
+ }
+ while (lo < hi) {
+ int mid = ((lo + hi) / 2).floor();
+ if (a[mid] < x) {
+ lo = mid + 1;
+ } else {
+ hi = mid;
+ }
+ }
+ return lo;
+ }
+
+ /**
+ * Returns the insertion point i for value x such that all values in a[lo:i]
+ * will be less than or equalto x and all values in a[i:hi] will be greater
+ * than x.
+ */
+ static int bisectRight(List a, num x, [int lo = 0, int hi = -1]) {
+ if (hi == -1) {
+ hi = a.length;
+ }
+ while (lo < hi) {
+ int mid = ((lo + hi) / 2).floor();
+ if (x < a[mid]) {
+ hi = mid;
+ } else {
+ lo = mid + 1;
+ }
+ }
+ return lo;
+ }
+
+ static Function bisect = bisectRight;
+}
+
+class Nice {
+ Function _floor;
+ Function _ceil;
+ Nice(Function this._ceil, Function this._floor);
+ set floor(Function f) {
+ _floor = f;
+ }
+ get floor => _floor;
+ set ceil(Function f) {
+ _ceil = f;
+ }
+ get ceil => _ceil;
+}
+
diff --git a/pub/charted/lib/core/lists.dart b/pub/charted/lib/core/lists.dart
new file mode 100644
index 0000000..e731df0
--- /dev/null
+++ b/pub/charted/lib/core/lists.dart
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.core;
+
+/** Returns a sum of all values in the given list of values */
+num sum(List values) =>
+ values == null || values.isEmpty ?
+ 0: values.fold(0.0, (old, next) => old + next);
+
+/** Returns the smallest number in the given list of values */
+num min(Iterable values) =>
+ values == null || values.isEmpty ?
+ null : values.fold(values.elementAt(0), math.min);
+
+/** Returns the largest number in the given list of values */
+num max(Iterable values) =>
+ values == null || values.isEmpty ?
+ null : values.fold(values.elementAt(0), math.max);
+
+/**
+ * Represents a tuple of mininum and maximum values in a List.
+ */
+class Extent<T> extends Pair<T, T> {
+ final T min;
+ final T max;
+
+ factory Extent.items(Iterable<T> items,
+ [ Comparator compare = Comparable.compare ]) {
+ if (items.length == 0) return new Extent(null, null);
+ var max = items.first,
+ min = items.first;
+ for (var value in items) {
+ if (compare(max, value) < 0) max = value;
+ if (compare(min, value) > 0) min = value;
+ }
+ return new Extent(min, max);
+ }
+
+ const Extent(T min, T max) : min = min, max = max, super(min, max);
+}
+
+/**
+ * Iterable representing a range of values containing the start, stop
+ * and each of the step values between them.
+ */
+class Range extends IterableBase<num> {
+ final List<num> _range = <num>[];
+
+ factory Range.integers(num start, [num stop, num step = 1]) =>
+ new Range(start, stop, step, true);
+
+ Range(num start, [num stop, num step = 1, bool integers = false]) {
+ if (stop == null) {
+ stop = start;
+ start = 0;
+ }
+
+ if (step == 0 || start < stop && step < 0 || start > stop && step > 0) {
+ throw new ArgumentError('Invalid range.');
+ }
+
+ var k = _integerConversionFactor(step.abs()),
+ i = -1,
+ j;
+
+ start *= k;
+ stop *= k;
+ step *= k;
+
+ if (step < 0) {
+ while ((j = start + step * ++i) > stop) {
+ _range.add(integers ? j ~/ k : j / k);
+ }
+ } else {
+ while ((j = start + step * ++i) < stop) {
+ _range.add(integers ? j ~/ k : j / k);
+ }
+ }
+ }
+
+ @override
+ int get length => _range.length;
+
+ @override
+ num elementAt(int index) => _range.elementAt(index);
+
+ @override
+ Iterator<num> get iterator => _range.iterator;
+
+ static int _integerConversionFactor(num val) {
+ int k = 1;
+ while (val * k % 1 > 0) {
+ k *= 10;
+ }
+ return k;
+ }
+}
diff --git a/pub/charted/lib/core/math.dart b/pub/charted/lib/core/math.dart
new file mode 100644
index 0000000..a5afa05
--- /dev/null
+++ b/pub/charted/lib/core/math.dart
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.core;
+
+const double PI = math.PI;
+const double HALF_PI = PI / 2.0;
+const double TAU = PI * 2.0;
+const double EPSILON = 1e-6;
+const double EPSILON_SQUARE = EPSILON * EPSILON;
+
+// Maximum (and minimum) value that would fit in Dart SMI
+const int SMALL_INT_MAX = (1 << 30) - 1;
+const int SMALL_INT_MIN = -1 * (1 << 30);
+
+num cosh(num x) => ((x = math.exp(x)) + 1 / x) / 2;
+num sinh(num x) => ((x = math.exp(x)) - 1 / x) / 2;
+num tanh(num x) => ((x = math.exp(2 * x)) - 1) / (x + 1);
+num toRadians(num degrees) => degrees * math.PI / 180.0;
+num toDegrees(num radians) => radians * 180.0 / math.PI;
diff --git a/pub/charted/lib/core/namespace.dart b/pub/charted/lib/core/namespace.dart
new file mode 100644
index 0000000..df6ece9
--- /dev/null
+++ b/pub/charted/lib/core/namespace.dart
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.core;
+
+/**
+ * Basic namespace handing for Charted - includes utilities to
+ * parse the namespace prefixes and to create DOM elements using a
+ * namespace.
+ */
+class Namespace {
+ /** Support namespace prefixes mapped to their URIs. */
+ static const Map<String,String> NS_PREFIXES = const {
+ "svg": "http://www.w3.org/2000/svg",
+ "xhtml": "http://www.w3.org/1999/xhtml",
+ "xlink": "http://www.w3.org/1999/xlink",
+ "xml": "http://www.w3.org/XML/1998/namespace",
+ "xmlns": "http://www.w3.org/2000/xmlns/"
+ };
+
+ /**
+ * Create an element from [tag]. If tag is prefixed with a
+ * supported namespace prefix, the created element will
+ * have the namespaceUri set to the correct URI.
+ */
+ static Element createChildElement(String tag, Element parent) {
+ Namespace parsed = new Namespace(tag);
+ return parsed.namespaceUri == null ?
+ parent.ownerDocument.createElementNS(parent.namespaceUri, tag) :
+ parent.ownerDocument.createElementNS(parsed.namespaceUri,
+ parsed.localName);
+ }
+
+ String localName;
+ String namespaceUri;
+
+ /**
+ * Parses a tag for namespace prefix and local name.
+ * If a known namespace prefix is found, sets the namespaceUri property
+ * to the URI of the namespace.
+ */
+ Namespace(String tagName) {
+ int separatorIdx = tagName.indexOf(':');
+ String prefix = tagName;
+ if (separatorIdx >= 0) {
+ prefix = tagName.substring(0, separatorIdx);
+ localName = tagName.substring(separatorIdx + 1);
+ }
+
+ if (NS_PREFIXES.containsKey(prefix)) {
+ namespaceUri = NS_PREFIXES[prefix];
+ } else {
+ localName = tagName;
+ }
+ }
+}
diff --git a/pub/charted/lib/core/object_factory.dart b/pub/charted/lib/core/object_factory.dart
new file mode 100644
index 0000000..c966858
--- /dev/null
+++ b/pub/charted/lib/core/object_factory.dart
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.core;
+
+typedef T ObjectCreator<T>();
+
+/**
+ * Provides a registry and factory service.
+ *
+ * Registration:
+ * ObjectFactory.register(“type”, () => { new TypeCreator(); });
+ *
+ * Usage:
+ * instance = ObjectFactory.create('type');
+ */
+class ObjectFactory<T> {
+ Map<String, ObjectCreator<T>> _components = {};
+
+ /**
+ * Registers a component.
+ */
+ void register(String name, ObjectCreator<T> creator) {
+ _components[name] = creator;
+ }
+
+ /**
+ * Creates an instance of the component.
+ */
+ T create(String name) {
+ if (!_components.containsKey(name)) {
+ throw new ArgumentError('Element $name not found in ComponentFactory');
+ }
+ var creator = _components[name],
+ instance = creator();
+ if (instance == null) {
+ throw new ArgumentError('Component $name initialization failed.');
+ }
+ return instance;
+ }
+}
diff --git a/pub/charted/lib/core/rect.dart b/pub/charted/lib/core/rect.dart
new file mode 100644
index 0000000..8e1e8e6
--- /dev/null
+++ b/pub/charted/lib/core/rect.dart
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.core;
+
+/** Interface representing size and position of an element */
+class Rect {
+ final num x;
+ final num y;
+ final num width;
+ final num height;
+
+ const Rect([this.x = 0, this.y = 0, this.width = 0, this.height = 0]);
+ const Rect.size(this.width, this.height) : x = 0, y = 0;
+ const Rect.position(this.x, this.y) : width = 0, height = 0;
+
+ bool operator==(other) =>
+ other is Rect && isSameSizeAs(other) && isSamePositionAs(other);
+
+ bool isSameSizeAs(Rect other) =>
+ other != null && width == other.width && height == other.height;
+
+ bool isSamePositionAs(Rect other) =>
+ other != null && x == other.x && y == other.y;
+
+ bool contains(num otherX, num otherY) =>
+ otherX >= x && otherX <= x + width &&
+ otherY >= y && otherY <= y + height;
+
+ String toString() => '$x, $y, $width, $height';
+}
+
+/**
+ * Editable Rect
+ * TODO(prsd): Can we avoid duplication here?
+ */
+class EditableRect implements Rect {
+ num x;
+ num y;
+ num width;
+ num height;
+
+ EditableRect(this.x, this.y, this.width, this.height);
+ EditableRect.size(this.width, this.height);
+ EditableRect.position(this.x, this.y);
+
+ bool operator==(other) =>
+ other is Rect && isSameSizeAs(other) && isSamePositionAs(other);
+
+ bool isSameSizeAs(Rect other) =>
+ other != null && width == other.width && height == other.height;
+
+ bool isSamePositionAs(Rect other) =>
+ other != null && x == other.x && y == other.y;
+
+ bool contains(num otherX, num otherY) =>
+ otherX >= x && otherX <= x + width &&
+ otherY >= y && otherY <= y + height;
+}
diff --git a/pub/charted/lib/event/event.dart b/pub/charted/lib/event/event.dart
new file mode 100644
index 0000000..c7272a7
--- /dev/null
+++ b/pub/charted/lib/event/event.dart
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+library charted.event;
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:html';
+
+part 'timer.dart';
diff --git a/pub/charted/lib/event/timer.dart b/pub/charted/lib/event/timer.dart
new file mode 100644
index 0000000..939801f
--- /dev/null
+++ b/pub/charted/lib/event/timer.dart
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.event;
+
+class ChartTimer {
+ static DoubleLinkedQueue<DoubleLinkedQueueEntry<ChartTimer>> _timerQueue =
+ new DoubleLinkedQueue<DoubleLinkedQueueEntry<ChartTimer>>();
+
+ static bool _interval = false;
+ static Timer _timer;
+ static var activeTimer;
+
+ Function _callback;
+ num _time;
+ bool _finished = false;
+
+ /**
+ * Start a custom animation timer, invoking the specified function
+ * repeatedly until it returns true. There is no way to cancel the timer
+ * after it starts, so make sure your timer function returns true when done!
+ *
+ * An optional numeric delay in milliseconds may be specified when the given
+ * function should only be invoked after a delay. The delay is relative to
+ * the specified time in milliseconds since UNIX epoch; if time is not
+ * specified, it defaults to Date.now
+ */
+ ChartTimer(this._callback, [int delay = 0, DateTime then = null]) {
+ if (then == null) {
+ then = new DateTime.now();
+ }
+ _time = then.add(new Duration(milliseconds: delay)).millisecondsSinceEpoch;
+ _timerQueue.add(new DoubleLinkedQueueEntry(this));
+
+ if (!_interval) {
+ if (_timer != null) {
+ _timer.cancel();
+ }
+ _interval = true;
+ window.animationFrame.then(_step);
+ }
+ }
+
+ /**
+ * Immediately execute (invoke once) any active timers. Normally, zero-delay
+ * transitions are executed after an instantaneous delay (<10ms). This can
+ * cause a brief flicker if the browser renders the page twice: once at the
+ * end of the first event loop, then again immediately on the first timer
+ * callback. By flushing the timer queue at the end of the first event loop,
+ * you can run any zero-delay transitions immediately and avoid the flicker.
+ */
+ void flush() {
+ _mark();
+ _sweep();
+ }
+
+ /**
+ * Interates through each of the timer and execute the callback if the set
+ * delay has elasped.
+ */
+ _mark() {
+ var now = new DateTime.now().millisecondsSinceEpoch;
+ for (DoubleLinkedQueueEntry e in _timerQueue) {
+ if (now > e.element._time) {
+ activeTimer = e.element;
+ e.element._finished = e.element._callback(now - e.element._time);
+ }
+ }
+ return now;
+ }
+
+ /**
+ * Flush after callbacks to avoid concurrent queue modification.
+ * Removes all finished timer from the queue and returns the time of the
+ * earliest active timer, post-sweep.
+ */
+ _sweep() {
+ var time = double.INFINITY;
+ for (DoubleLinkedQueueEntry e in _timerQueue) {
+ if (e.element._finished) {
+ _timerQueue.remove(e);
+ } else {
+ if (e.element._time < time) {
+ time = e.element._time;
+ }
+ }
+ }
+ return time;
+ }
+
+ /*
+ * If the delay of the timer in the nearest future is less than 24ms, use
+ * animationFrame to step, otherwise schedule the step of the chart timer
+ * using a Dart Timer with the delay.
+ */
+ _step([num delta = 0]) {
+ var now = _mark(),
+ delay = _sweep() - now;
+
+ if (delay > 24) {
+ // If delay is infinity there's no more timer in queue.
+ if (delay.isFinite) {
+ if (_timer != null) {
+ _timer.cancel();
+ }
+ _timer = new Timer(new Duration(milliseconds: delay), _step);
+ }
+ _interval = false;
+ activeTimer = null;
+ }
+ else {
+ _interval = true;
+ window.animationFrame.then(_step);
+ }
+ }
+}
+
diff --git a/pub/charted/lib/format/format.dart b/pub/charted/lib/format/format.dart
new file mode 100644
index 0000000..52bdd5e
--- /dev/null
+++ b/pub/charted/lib/format/format.dart
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+library charted.format;
+
+import 'dart:math' as math;
+import 'package:charted/locale/locale.dart';
+
+typedef String NumberFormatFunction(num x, [int precision]);
+typedef String FormatFunction(num x);
+
+/**
+ * Returns a new format function with the given string specifier.
+ * The format specifier is modeled after Python 3.1's built-in format
+ * specification mini-language.
+ *
+ * The general form of a specifier is:
+ * [​[fill]align][sign][symbol][0][width][,][.precision][type]
+ *
+ * @see <a href="http://docs.python.org/release/3.1.3/library/string.html#formatspec">Python format specification mini-language</a>
+ */
+FormatFunction format(String specifier, [Locale locale = null]) {
+ if (locale == null) {
+ locale = new EnusLocale();
+ }
+ return locale.numberFormat.format(specifier);
+}
+
+
+/*
+ * Class for computing the SI format prefix for the given value.
+ */
+class FormatPrefix {
+ // SI scale units in increments of 1000.
+ static const List unitPrefixes = const
+ ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];
+
+ Function _scale;
+ String _symbol;
+
+ FormatPrefix(num value, [int precision = 0]) {
+ var i = 0;
+ if (value < 0) {
+ value *= -1;
+ }
+ if (precision > 0) {
+ value = _roundToPrecision(value, _formatPrecision(value, precision));
+ }
+
+ // Determining SI scale of the value in increment of 1000.
+ i = 1 + (1e-12 + math.log(value) / math.LN10).floor();
+ i = math.max(-24, math.min(24,
+ ((i - 1) / 3).floor() * 3));
+ i = 8 + (i / 3).floor();
+
+ // Sets the scale and symbol of the value.
+ var k = math.pow(10, (8 - i).abs() * 3);
+ _scale = i > 8 ? (d) => d / k : (d) => d * k;
+ _symbol = unitPrefixes[i];
+ }
+
+ _formatPrecision(num x, num p) {
+ return p - (x != 0 ? (math.log(x) / math.LN10).ceil() : 1);
+ }
+
+ /** Returns the value of x rounded to the nth digit. */
+ _roundToPrecision(num x, num n) {
+ return n != 0 ?
+ (x * (n = math.pow(10, n))).round() / n : x.round();
+ }
+
+ /** Returns the SI prefix for the value. */
+ get symbol => _symbol;
+
+ /** Returns the scale for the value corresponding to the SI prefix. */
+ get scale => _scale;
+}
diff --git a/pub/charted/lib/interpolators/easing_impl.dart b/pub/charted/lib/interpolators/easing_impl.dart
new file mode 100644
index 0000000..fbbc45d
--- /dev/null
+++ b/pub/charted/lib/interpolators/easing_impl.dart
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.interpolators;
+
+EasingFn clampEasingFn(EasingFn f) =>
+ (t) => t <= 0 ? 0 : t >= 1 ? 1 : f(t);
+
+// Ease-in is defined is the identifyFunction.
+/** Ease-out */
+EasingFn reverseEasingFn(EasingFn f) =>
+ (t) => 1 - f(1 - t);
+
+/** Ease-in-out */
+EasingFn reflectEasingFn(EasingFn f) =>
+ (t) => .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t)));
+
+/** Ease-out-in */
+EasingFn reflectReverseEasingFn(EasingFn f) =>
+ reflectEasingFn(reverseEasingFn(f));
+
+EasingFn easePoly([e = 1]) => (t) => math.pow(t, e);
+
+EasingFn easeElastic([a = 1, p = 0.45]) {
+ var s = p / 2 * math.PI * math.asin(1 / a);
+ return (t) => 1 + a * math.pow(2, -10 * t) *
+ math.sin((t - s) * 2 * math.PI / p);
+}
+
+EasingFn easeBack([s = 1.70158]) =>
+ (num t) => t * t * ((s + 1) * t - s);
+
+EasingFn easeQuad() => (num t) => t * t;
+
+EasingFn easeCubic() => (num t) => t * t * t;
+
+EasingFn easeCubicInOut() =>
+ (num t) {
+ if (t <= 0) return 0;
+ if (t >= 1) return 1;
+ var t2 = t * t,
+ t3 = t2 * t;
+ return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75);
+ };
+
+EasingFn easeSin() =>
+ (num t) => 1 - math.cos(t * math.PI / 2);
+
+EasingFn easeExp() =>
+ (num t) => math.pow(2, 10 * (t - 1));
+
+EasingFn easeCircle() =>
+ (num t) => 1 - math.sqrt(1 - t * t);
+
+EasingFn easeBounce() =>
+ (num t) => t < 1 / 2.75 ?
+ 7.5625 * t * t : t < 2 / 2.75 ?
+ 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ?
+ 7.5625 * (t -= 2.25 / 2.75) * t + .9375
+ : 7.5625 * (t -= 2.625 / 2.75) * t + .984375;
diff --git a/pub/charted/lib/interpolators/interpolators.dart b/pub/charted/lib/interpolators/interpolators.dart
new file mode 100644
index 0000000..b9c1473
--- /dev/null
+++ b/pub/charted/lib/interpolators/interpolators.dart
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+library charted.interpolators;
+
+import 'dart:math' as math;
+import 'package:charted/core/core.dart';
+import 'package:csslib/parser.dart' as cssParser;
+
+part 'interpolators_impl.dart';
+part 'easing_impl.dart';
+
+/**
+ * [InterpolateFn] accepts [t], such that 0.0 < t < 1.0 and returns
+ * a value in a pre-defined range.
+ */
+typedef InterpolateFn(num t);
+
+/**
+ * [Interpolator] accepts two parameters [a], [b] and returns a function
+ * that takes a number t, such that 0.0 <= t <= 1.0 and interpolates it
+ * to a value between [a] and [b]
+ */
+typedef InterpolateFn Interpolator(a, b);
+
+/** [EasingFn] is same as [InterpolateFn] but is for computing easing */
+typedef num EasingFn(num t);
+
+/**
+ * [EasingMode] a mode that can be applied on a [Easingfn] and returns a new
+ * EasingFn.
+ */
+typedef EasingFn EasingMode(EasingFn fn);
+
+/**
+ * List of registered interpolators - [interpolator] iterates through
+ * this list from backwards and the first non-null interpolate function
+ * is returned to the caller.
+ */
+List<Interpolator> interpolators = [ interpolatorByType ];
+
+/**
+ * Returns a default interpolator between values [a] and [b]. Unless
+ * more interpolators are added, one of the internal implementations are
+ * selected by the type of [a] and [b].
+ */
+InterpolateFn interpolator(a, b) {
+ var fn, i = interpolators.length;
+ while (--i >= 0 && fn == null) {
+ fn = interpolators[i](a, b);
+ }
+ return fn;
+}
+
+/** Returns an interpolator based on the type of [a] and [b] */
+InterpolateFn interpolatorByType(a, b) =>
+ (a is List && b is List) ? interpolateList(a, b) :
+ (a is Map && b is Map) ? interpolateMap(a, b) :
+ (a is String && b is String) ? interpolateString(a, b) :
+ (a is num && b is num) ? interpolateNumber(a, b) :
+ (a is Color && b is Color) ? interpolateColor(a, b) :
+ (t) => (t <= 0.5) ? a : b;
+
+/*
+ * Creates an easing function based on type and mode.
+ * Assumes that all easing function generators support calling
+ * without any parameters.
+ */
+EasingFn easeFunctionByName(String type,
+ [String mode = EASE_MODE_IN, List params]) {
+ const Map _easeType = const {
+ 'linear': identityFunction,
+ 'poly': easePoly,
+ 'quad': easeQuad,
+ 'cubic': easeCubic,
+ 'sin': easeSin,
+ 'exp': easeExp,
+ 'circle': easeCircle,
+ 'elastic': easeElastic,
+ 'back': easeBack,
+ 'bounce': easeBounce
+ };
+
+ const Map _easeMode = const {
+ 'in': identityFunction,
+ 'out': reverseEasingFn,
+ 'in-out': reflectEasingFn,
+ 'out-in': reflectReverseEasingFn
+ };
+
+ assert(_easeType.containsKey(type));
+ assert(_easeMode.containsKey(mode));
+
+ var fn = Function.apply(_easeType[type], params);
+ return clampEasingFn(_easeMode[mode](fn));
+}
diff --git a/pub/charted/lib/interpolators/interpolators_impl.dart b/pub/charted/lib/interpolators/interpolators_impl.dart
new file mode 100644
index 0000000..8d3e86a
--- /dev/null
+++ b/pub/charted/lib/interpolators/interpolators_impl.dart
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.interpolators;
+
+/** Returns a numeric interpolator between numbers [a] and [b] */
+InterpolateFn interpolateNumber(num a, num b) {
+ b -= a;
+ return (t) => a + b * t;
+}
+
+/** Returns a rounded number interpolator between numbers [a] and [b] */
+InterpolateFn interpolateRound(num a, num b) {
+ b -= a;
+ return (t) => (a + b * t).round();
+}
+
+/**
+ * Returns the interpolator between two strings [a] and [b].
+ *
+ * The interpolator will interpolate all the number pairs in both strings
+ * that have same number of numeric parts. The function assumes the non
+ * number part of the string to be identical and would use string [b] for
+ * merging the non numeric part of the strings.
+ *
+ * Eg: Interpolate between $100.0 and $150.0
+ */
+InterpolateFn interpolateString(String a, String b) {
+ if (a == null || b == null) return (t) => b;
+ if (Color.isColorString(a) && Color.isColorString(b)) {
+ return interpolateColor(new Color.fromColorString(a),
+ new Color.fromColorString(b));
+ }
+ var numberRegEx =
+ new RegExp(r'[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?'),
+ numMatchesInA = numberRegEx.allMatches(a),
+ numMatchesInB = numberRegEx.allMatches(b),
+ stringParts = [],
+ numberPartsInA = [],
+ numberPartsInB = [],
+ interpolators = [],
+ s0 = 0;
+
+ // Get all the non number parts in a.
+ numberPartsInA.addAll(numMatchesInA.map((m) => m.group(0)));
+
+ // Get all the non number parts in b and record string parts.
+ for (Match m in numMatchesInB) {
+ stringParts.add(b.substring(s0, m.start));
+ numberPartsInB.add(m.group(0));
+ s0 = m.end;
+ }
+
+ if (s0 < b.length) stringParts.add(b.substring(s0));
+
+ int numberLength = math.min(numberPartsInA.length, numberPartsInB.length);
+ int maxLength = math.max(numberPartsInA.length, numberPartsInB.length);
+ for (var i = 0; i < numberLength; i++) {
+ interpolators.add(interpolateNumber(num.parse(numberPartsInA[i]),
+ num.parse(numberPartsInB[i])));
+ }
+ if (numberPartsInA.length < numberPartsInB.length) {
+ for (var i = numberLength; i < maxLength; i++) {
+ interpolators.add(interpolateNumber(num.parse(numberPartsInB[i]),
+ num.parse(numberPartsInB[i])));
+ }
+ }
+
+ return (t) {
+ StringBuffer sb = new StringBuffer();
+ for (var i = 0; i < stringParts.length; i++) {
+ sb.write(stringParts[i]);
+ if (interpolators.length > i) {
+ sb.write(interpolators[i](t));
+ }
+ }
+ return sb.toString();
+ };
+}
+
+/** Returns the interpolator for RGB values. */
+InterpolateFn interpolateColor(Color a, Color b) {
+ if (a == null || b == null) return (t) => b;
+ var ar = a.r,
+ ag = a.g,
+ ab = a.b,
+ br = b.r - ar,
+ bg = b.g - ag,
+ bb = b.b - ab;
+
+ return (t) => new Color.fromRgb(
+ (ar + br * t).round(), (ag + bg * t).round(), (ab + bb * t).round());
+}
+
+/** Returns the interpolator using HSL color system converted to Hex string. */
+InterpolateFn interpolateHsl(a, b) {
+ if (a == null || b == null) return (t) => b;
+ if (a is String && Color.isColorString(a)) a = new Color.fromColorString(a);
+ if (a is Color) a = new cssParser.Hsla.fromString(a.hexString);
+ if (b is String && Color.isColorString(b)) b = new Color.fromColorString(b);
+ if (b is Color) b = new cssParser.Hsla.fromString(b.hexString);
+
+ var ah = a.hue,
+ as = a.saturation,
+ al = a.lightness,
+ bh = b.hue - ah,
+ bs = b.saturation - as,
+ bl = b.lightness - al;
+
+ return (t) => "#" + new cssParser.Hsla(
+ ah + bh * t, as + bs * t, al + bl * t).toHexArgbString();
+}
+
+/**
+ * Returns the interpolator that interpolators each element between lists
+ * [a] and [b] using registered interpolators.
+ */
+InterpolateFn interpolateList(List a, List b) {
+ if (a == null || b == null) return (t) => b;
+ var x = [],
+ na = a.length,
+ nb = b.length,
+ n0 = math.min(na, nb),
+ c = new List.filled(math.max(na, nb), null),
+ i;
+
+ for (i = 0; i < n0; i++) x.add(interpolator(a[i], b[i]));
+ for (; i < na; ++i) c[i] = a[i];
+ for (; i < nb; ++i) c[i] = b[i];
+
+ return (t) {
+ for (i = 0; i < n0; ++i) c[i] = x[i](t);
+ return c;
+ };
+}
+
+/**
+ * Returns the interpolator that interpolators each element between maps
+ * [a] and [b] using registered interpolators.
+ */
+InterpolateFn interpolateMap(Map a, Map b) {
+ if (a == null || b == null) return (t) => b;
+ var x = new Map(),
+ c = new Map(),
+ ka = a.keys.toList(),
+ kb = b.keys.toList();
+
+ ka.forEach((k) {
+ if (b[k] != null) x[k] = (interpolator(a[k], b[k]));
+ else c[k] = a[k];
+ });
+ kb.forEach((k) {
+ if (c[k] == null) c[k] = b[k];
+ });
+
+ return (t) {
+ x.forEach((k, v) => c[k] = v(t));
+ return c;
+ };
+}
+
+InterpolateFn uninterpolateNumber(num a, num b) {
+ b = 1 / (b - a);
+ return (x) => (x - a) * b;
+}
+
+InterpolateFn uninterpolateClamp(num a, num b) {
+ b = 1 / (b - a);
+ return (x) => math.max(0, math.min(1, (x - a) * b));
+}
+
+/**
+ * Returns the interpolator that interpolators two transform strings
+ * [a] and [b] by their translate, rotate, scale and skewX parts.
+ */
+InterpolateFn interpolateTransform(String a, String b) {
+ if (a == null || b == null) return (t) => b;
+ var numRegExStr = r'[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?',
+ translateRegExStr =
+ r'translate\(' + numRegExStr + r',' + numRegExStr + r'\)',
+ scaleRegExStr = r'scale\(' + numRegExStr + r',' + numRegExStr + r'\)',
+ rotateRegExStr = r'rotate\(' + numRegExStr + r'(deg)?\)',
+ skewRegExStr = r'skewX\(' + numRegExStr + r'(deg)?\)',
+
+ numberRegEx = new RegExp(numRegExStr),
+ translateRegEx = new RegExp(translateRegExStr),
+ scaleRegEx = new RegExp(scaleRegExStr),
+ rotateRegEx = new RegExp(rotateRegExStr),
+ skewRegEx = new RegExp(skewRegExStr),
+
+ translateA = translateRegEx.firstMatch(a),
+ scaleA = scaleRegEx.firstMatch(a),
+ rotateA = rotateRegEx.firstMatch(a),
+ skewA = skewRegEx.firstMatch(a),
+
+ translateB = translateRegEx.firstMatch(b),
+ scaleB = scaleRegEx.firstMatch(b),
+ rotateB = rotateRegEx.firstMatch(b),
+ skewB = skewRegEx.firstMatch(b);
+
+ var numSetA = [],
+ numSetB = [],
+ tempStr, match;
+
+ // translate
+ if (translateA != null) {
+ tempStr = a.substring(translateA.start, translateA.end);
+ match = numberRegEx.allMatches(tempStr);
+ for (Match m in match) {
+ numSetA.add(num.parse(m.group(0)));
+ }
+ } else {
+ numSetA.addAll(const[0, 0]);
+ }
+
+ if (translateB != null) {
+ tempStr = b.substring(translateB.start, translateB.end);
+ match = numberRegEx.allMatches(tempStr);
+ for (Match m in match) {
+ numSetB.add(num.parse(m.group(0)));
+ }
+ } else {
+ numSetB.addAll(const[0, 0]);
+ }
+
+ // scale
+ if (scaleA != null) {
+ tempStr = a.substring(scaleA.start, scaleA.end);
+ match = numberRegEx.allMatches(tempStr);
+ for (Match m in match) {
+ numSetA.add(num.parse(m.group(0)));
+ }
+ } else {
+ numSetA.addAll(const[1, 1]);
+ }
+
+ if (scaleB != null) {
+ tempStr = b.substring(scaleB.start, scaleB.end);
+ match = numberRegEx.allMatches(tempStr);
+ for (Match m in match) {
+ numSetB.add(num.parse(m.group(0)));
+ }
+ } else {
+ numSetB.addAll(const[1, 1]);
+ }
+
+ // rotate
+ if (rotateA != null) {
+ tempStr = a.substring(rotateA.start, rotateA.end);
+ match = numberRegEx.firstMatch(tempStr);
+ numSetA.add(num.parse(match.group(0)));
+ } else {
+ numSetA.add(0);
+ }
+
+ if (rotateB != null) {
+ tempStr = b.substring(rotateB.start, rotateB.end);
+ match = numberRegEx.firstMatch(tempStr);
+ numSetB.add(num.parse(match.group(0)));
+ } else {
+ numSetB.add(0);
+ }
+
+ // rotate < 180 degree
+ if (numSetA[4] != numSetB[4]) {
+ if (numSetA[4] - numSetB[4] > 180) {
+ numSetB[4] += 360;
+ } else if (numSetB[4] - numSetA[4] > 180) {
+ numSetA[4] += 360;
+ }
+ }
+
+ // skew
+ if (skewA != null) {
+ tempStr = a.substring(skewA.start, skewA.end);
+ match = numberRegEx.firstMatch(tempStr);
+ numSetA.add(num.parse(match.group(0)));
+ } else {
+ numSetA.add(0);
+ }
+
+ if (skewB != null) {
+ tempStr = b.substring(skewB.start, skewB.end);
+ match = numberRegEx.firstMatch(tempStr);
+ numSetB.add(num.parse(match.group(0)));
+ } else {
+ numSetB.add(0);
+ }
+
+ return (t) {
+ return "translate("+
+ interpolateNumber(numSetA[0], numSetB[0])(t).toString()+"," +
+ interpolateNumber(numSetA[1], numSetB[1])(t).toString()+")scale("+
+ interpolateNumber(numSetA[2], numSetB[2])(t).toString()+","+
+ interpolateNumber(numSetA[3], numSetB[3])(t).toString()+")rotate("+
+ interpolateNumber(numSetA[4], numSetB[4])(t).toString()+")skewX("+
+ interpolateNumber(numSetA[5], numSetB[5])(t).toString()+")";
+ };
+}
+
+/**
+ * Returns the interpolator that interpolators two Zoom lists [a] and [b].
+ * [a] and [b] are described by triple elements
+ * [ux0, uy0, w0] and [ux1, uy1, w1].
+ */
+InterpolateFn interpolateZoom(List a, List b) {
+ if (a == null || b == null) return (t) => b;
+ assert(a.length == b.length && a.length == 3);
+
+ var sqrt2 = math.SQRT2,
+ param2 = 2,
+ param4 = 4;
+
+ var ux0 = a[0], uy0 = a[1], w0 = a[2],
+ ux1 = b[0], uy1 = b[1], w1 = b[2];
+
+ var dx = ux1 - ux0,
+ dy = uy1 - uy0,
+ d2 = dx * dx + dy * dy,
+ d1 = math.sqrt(d2),
+ b0 = (w1 * w1 - w0 * w0 + param4 * d2) / (2 * w0 * param2 * d1),
+ b1 = (w1 * w1 - w0 * w0 - param4 * d2) / (2 * w1 * param2 * d1),
+ r0 = math.log(math.sqrt(b0 * b0 + 1) - b0),
+ r1 = math.log(math.sqrt(b1 * b1 + 1) - b1),
+ dr = r1 - r0,
+ S = ((!dr.isNaN) ? dr : math.log(w1 / w0)) / sqrt2;
+
+ return (t) {
+ var s = t * S;
+ if (!dr.isNaN) {
+ // General case.
+ var coshr0 = cosh(r0),
+ u = w0 / (param2 * d1) * (coshr0 * tanh(sqrt2 * s + r0) - sinh(r0));
+ return [
+ ux0 + u * dx,
+ uy0 + u * dy,
+ w0 * coshr0 / cosh(sqrt2 * s + r0)
+ ];
+ }
+ // Special case for u0 ~= u1.
+ return [
+ ux0 + t * dx,
+ uy0 + t * dy,
+ w0 * math.exp(sqrt2 * s)
+ ];
+ };
+}
diff --git a/pub/charted/lib/layout/layout.dart b/pub/charted/lib/layout/layout.dart
new file mode 100644
index 0000000..ecfbd1e
--- /dev/null
+++ b/pub/charted/lib/layout/layout.dart
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+/*
+ * TODO(prsd): Document library
+ */
+library charted.layout;
+
+import 'dart:html' show Element;
+import 'package:charted/core/core.dart';
+import 'package:charted/selection/selection.dart';
+import 'package:charted/svg/svg.dart' show SvgArcData;
+
+part 'pie_layout.dart';
\ No newline at end of file
diff --git a/pub/charted/lib/layout/pie_layout.dart b/pub/charted/lib/layout/pie_layout.dart
new file mode 100644
index 0000000..fcedbd4
--- /dev/null
+++ b/pub/charted/lib/layout/pie_layout.dart
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.layout;
+
+/**
+ * Utility class to create arc definitions that can be used by the SvgArc
+ * to generate arcs for pie and donut charts
+ */
+class PieLayout {
+ /**
+ * Callback to convert datum to values used for layout
+ * Defaults to [defaultValueAccessor]
+ */
+ SelectionValueAccessor<num> accessor = defaultValueAccessor;
+
+ /**
+ * Callback to get the start angle for the pie. This callback is
+ * called once per list of value (i.e once per call to [layout])
+ * Defaults to [defaultStartAngleCallback]
+ */
+ SelectionCallback<num> startAngleCallback = defaultStartAngleCallback;
+
+ /**
+ * Callback to get the start angle for the pie. This callback is
+ * called once per list of value (i.e once per call to [layout])
+ * Defaults to [defaultEndAngleCallback]
+ */
+ SelectionCallback<num> endAngleCallback = defaultEndAngleCallback;
+
+ /**
+ * Comparator that is used to set the sort order of values. If not
+ * specified, the input order is used.
+ */
+ Comparator<num> compare = null;
+
+ /**
+ * Return a list of SvgArcData objects that could be used to create
+ * arcs in a pie-chart or donut-chart.
+ */
+ List layout(List data, [int ei, Element e]) {
+ var values = new List.generate(data.length,
+ (int i) => accessor(data[i], i)),
+ startAngle = startAngleCallback(data, ei, e),
+ endAngle = endAngleCallback(data, ei, e),
+ total = sum(values),
+ scaleFactor = (endAngle - startAngle) / (total > 0 ? total : 1),
+ index = new Range.integers(values.length).toList(),
+ arcs = new List(data.length);
+
+ if (compare != null) {
+ index.sort((left, right) => compare(data[left], data[right]));
+ }
+
+ int count = 0;
+ index.forEach((i) {
+ endAngle = startAngle + values[i] * scaleFactor;
+ arcs[count++] = new SvgArcData(data[i], values[i], startAngle, endAngle);
+ startAngle = endAngle;
+ });
+
+ return arcs;
+ }
+
+ /** Sets a constant value to start angle of the layout */
+ set startAngle(num value) =>
+ startAngleCallback = toCallback(value);
+
+ /** Sets a constant value to end angle of the layout */
+ set endAngle(num value) =>
+ endAngleCallback = toCallback(value);
+
+ /** Default value accessor */
+ static num defaultValueAccessor(num d, i) => d;
+
+ /** Default start angle callback - returns 0 */
+ static num defaultStartAngleCallback(d, i, _) => 0;
+
+ /** Default end angle calback - returns 2 * PI */
+ static num defaultEndAngleCallback(d, i, _) => 2 * PI;
+}
diff --git a/pub/charted/lib/locale/en_us.dart b/pub/charted/lib/locale/en_us.dart
new file mode 100644
index 0000000..e37d7ab
--- /dev/null
+++ b/pub/charted/lib/locale/en_us.dart
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.locale;
+
+// Defines the en_us locale and related format properties.
+class EnusLocale extends Locale {
+ get identifier => 'en_US';
+ get decimal => '.';
+ get thousands => ',';
+ get grouping => [3];
+ get currency => ['\$', ''];
+ get dateTime => '%a %b %e %X %Y';
+ get date => '%m/%d/%Y';
+ get time => '%H =>%M =>%S';
+ get periods => ['AM', 'PM'];
+ get days => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+ get shortDays => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+ get months => ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+ get shortMonths => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+}
diff --git a/pub/charted/lib/locale/locale.dart b/pub/charted/lib/locale/locale.dart
new file mode 100644
index 0000000..d555ed0
--- /dev/null
+++ b/pub/charted/lib/locale/locale.dart
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+/*
+ * TODO(midoringo): Document library
+ */
+library charted.locale;
+
+import 'dart:math' as math;
+import 'package:charted/core/core.dart';
+import 'package:charted/format/format.dart';
+import 'package:charted/time/time.dart' as chartTime;
+import 'package:charted/scale/scale.dart';
+import 'package:intl/date_symbol_data_local.dart';
+import 'package:intl/intl.dart';
+
+import 'package:charted/interpolators/interpolators.dart' as interpolators;
+
+part 'en_us.dart';
+part 'number_format.dart';
+part 'time_format.dart';
+part 'time_scale.dart';
+
+abstract class Locale {
+ String get identifier;
+ String get decimal;
+ String get thousands;
+ List get grouping;
+ List get currency;
+ String get dateTime;
+ String get date;
+ String get time;
+ List get periods;
+ List get days;
+ List get shortDays;
+ List get months;
+ List get shortMonths;
+
+ Locale() {
+ initializeDateFormatting(this.identifier, null);
+ }
+
+ NumberFormat get numberFormat => new NumberFormat(this);
+ // TODO(songrenchu): port time format.
+ TimeFormat timeFormat([specifier = null]) =>
+ new TimeFormat(specifier, this.identifier);
+
+}
diff --git a/pub/charted/lib/locale/number_format.dart b/pub/charted/lib/locale/number_format.dart
new file mode 100644
index 0000000..fcdf29d
--- /dev/null
+++ b/pub/charted/lib/locale/number_format.dart
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.locale;
+
+typedef String NumberFormatFunction(num x, [int precision]);
+typedef String FormatFunction(num x);
+
+// TODO(midoringo): Add more test on NumberFormat.
+
+/**
+ * The number formatter of a given locale. Applying the locale specific
+ * number format, number grouping and currency symbol, etc.. The format
+ * function in the NumberFormat class is used to format a number by the given
+ * specifier with the number properties of the locale.
+ */
+class NumberFormat {
+
+ // [[fill]align][sign][symbol][0][width][,][.precision][type]
+ static RegExp FORMAT_REGEX =
+ new RegExp(r'(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?'
+ r'(\.-?\d+)?([a-z%])?', caseSensitive: false);
+
+ String localeDecimal;
+ String localeThousands;
+ List localeGrouping;
+ List localeCurrency;
+ Function formatGroup;
+
+ NumberFormat(Locale locale) {
+ localeDecimal = locale.decimal;
+ localeThousands = locale.thousands;
+ localeGrouping = locale.grouping;
+ localeCurrency = locale.currency;
+ formatGroup = (localeGrouping != null) ? (value) {
+ var i = value.length,
+ t = [],
+ j = 0,
+ g = localeGrouping[0];
+ while (i > 0 && g > 0) {
+ if (i - g >= 0) {
+ i = i - g;
+ } else {
+ g = i;
+ i = 0;
+ }
+ var length = (i + g) < value.length ? (i + g) : value.length;
+ t.add(value.substring(i, length));
+ g = localeGrouping[j = (j + 1) % localeGrouping.length];
+ }
+ return t.reversed.join(localeThousands);
+ } : (x) => x;
+ }
+
+ /**
+ * Returns a new format function with the given string specifier. A format
+ * function takes a number as the only argument, and returns a string
+ * representing the formatted number. The format specifier is modeled after
+ * Python 3.1's built-in format specification mini-language. The general form
+ * of a specifier is:
+ * [​[fill]align][sign][symbol][0][width][,][.precision][type].
+ *
+ * @see <a href="http://docs.python.org/release/3.1.3/library/string.html#formatspec">format specification mini-language</a>
+ */
+ FormatFunction format(String specifier) {
+ Match match = FORMAT_REGEX.firstMatch(specifier);
+ var fill = match.group(1) != null ? match.group(1) : ' ',
+ align = match.group(2) != null ? match.group(2) : '>',
+ sign = match.group(3) != null ? match.group(3) : '',
+ symbol = match.group(4) != null ? match.group(4) : '',
+ zfill = match.group(5),
+ width = match.group(6) != null ? int.parse(match.group(6)) : 0,
+ comma = match.group(7) != null,
+ precision = match.group(8) != null ?
+ int.parse(match.group(8).substring(1)) : null,
+ type = match.group(9),
+ scale = 1,
+ prefix = '',
+ suffix = '',
+ integer = false;
+
+ if (zfill != null || fill == '0' && align == '=') {
+ zfill = fill = '0';
+ align = '=';
+ if (comma) {
+ width -= ((width - 1) / 4).floor();
+ }
+ }
+
+ switch (type) {
+ case 'n': comma = true; type = 'g'; break;
+ case '%': scale = 100; suffix = '%'; type = 'f'; break;
+ case 'p': scale = 100; suffix = '%'; type = 'r'; break;
+ case 'b':
+ case 'o':
+ case 'x':
+ case 'X': if (symbol == '#') prefix = '0' + type.toLowerCase(); break;
+ case 'c':
+ case 'd': integer = true; precision = 0; break;
+ case 's': scale = -1; type = 'r'; break;
+ }
+
+ if (symbol == '\$') {
+ prefix = localeCurrency[0];
+ suffix = localeCurrency[1];
+ }
+
+ // If no precision is specified for r, fallback to general notation.
+ if (type == 'r' && precision == null) {
+ type = 'g';
+ }
+
+ // Ensure that the requested precision is in the supported range.
+ if (precision != null) {
+ if (type == 'g') {
+ precision = math.max(1, math.min(21, precision));
+ } else if (type == 'e' || type == 'f') {
+ precision = math.max(0, math.min(20, precision));
+ }
+ }
+
+ NumberFormatFunction formatFunction = _getFormatFunction(type);
+
+ var zcomma = (zfill != null) && comma;
+
+ return (value) {
+ var fullSuffix = suffix;
+
+ // Return the empty string for floats formatted as ints.
+ if (integer && (value % 1) > 0) return '';
+
+ // Convert negative to positive, and record the sign prefix.
+ var negative;
+ if (value < 0 || value == 0 && 1 / value < 0) {
+ value = -value;
+ negative = '-';
+ } else {
+ negative = sign;
+ }
+
+ // Apply the scale, computing it from the value's exponent for si
+ // format. Preserve the existing suffix, if any, such as the
+ // currency symbol.
+ if (scale < 0) {
+ FormatPrefix unit = new FormatPrefix(value,
+ (precision != null) ? precision : 0);
+ value = unit.scale(value);
+ fullSuffix = unit.symbol + suffix;
+ } else {
+ value *= scale;
+ }
+
+ // Convert to the desired precision.
+ if (precision != null) {
+ value = formatFunction(value, precision);
+ } else {
+ value = formatFunction(value);
+ }
+
+ // Break the value into the integer part (before) and decimal part
+ // (after).
+ var i = value.lastIndexOf('.'),
+ before = i < 0 ? value : value.substring(0, i),
+ after = i < 0 ? '' : localeDecimal + value.substring(i + 1);
+
+ // If the fill character is not '0', grouping is applied before
+ //padding.
+ if (zfill == null && comma) {
+ before = formatGroup(before);
+ }
+
+ var length = prefix.length + before.length + after.length +
+ (zcomma ? 0 : negative.length),
+ padding = length < width ? new List.filled(
+ (length = width - length + 1), '').join(fill) : '';
+
+ // If the fill character is '0', grouping is applied after padding.
+ if (zcomma) {
+ before = formatGroup(padding + before);
+ }
+
+ // Apply prefix.
+ negative += prefix;
+
+ // Rejoin integer and decimal parts.
+ value = before + after;
+
+ // Apply any padding and alignment attributes before returning the string.
+ return (align == '<' ? negative + value + padding
+ : align == '>' ? padding + negative + value
+ : align == '^' ? padding.substring(0, length >>= 1) + negative +
+ value + padding.substring(length)
+ : negative + (zcomma ? value : padding + value)) + fullSuffix;
+ };
+ }
+
+ // Gets the format function by given type.
+ NumberFormatFunction _getFormatFunction(String type) {
+ switch(type) {
+ case 'b':
+ return (num x, [int p = 0]) => x.toInt().toRadixString(2);
+ case 'c':
+ return (num x, [int p = 0]) => new String.fromCharCodes([x]);
+ case 'o':
+ return (num x, [int p = 0]) => x.toInt().toRadixString(8);
+ case 'x':
+ return (num x, [int p = 0]) => x.toInt().toRadixString(16);
+ case 'X':
+ return (num x, [int p = 0]) =>
+ x.toInt().toRadixString(16).toUpperCase();
+ case 'g':
+ return (num x, [int p = 1]) => x.toStringAsPrecision(p);
+ case 'e':
+ return (num x, [int p = 0]) => x.toStringAsExponential(p);
+ case 'f':
+ return (num x, [int p = 0]) => x.toStringAsFixed(p);
+ case 'r':
+ default:
+ return (num x, [int p = 0]) => x.toString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/pub/charted/lib/locale/time_format.dart b/pub/charted/lib/locale/time_format.dart
new file mode 100644
index 0000000..5bd5d8b
--- /dev/null
+++ b/pub/charted/lib/locale/time_format.dart
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.locale;
+
+typedef String TimeFormatFunction(DateTime date);
+
+//TODO(songrenchu): Document time format; Add test for time format.
+
+class TimeFormat {
+ String _template;
+ String _locale;
+ DateFormat _dateFormat;
+
+ TimeFormat([String template = null, String identifier = 'en_US']) {
+ _template = template;
+ _locale = identifier;
+ if (_template != null)
+ _dateFormat = new DateFormat(_wrapStrptime2ICU(_template), _locale);
+ }
+
+ TimeFormat _getInstance(String template) {
+ return new TimeFormat(template, _locale);
+ }
+
+ String apply(DateTime date) {
+ assert(_dateFormat != null);
+ return _dateFormat.format(date);
+ }
+
+ String toString() => _template;
+
+ DateTime parse(String string) {
+ assert(_dateFormat != null);
+ return _dateFormat.parse(string);
+ }
+
+ TimeFormatFunction multi(List<List> formats) {
+ var n = formats.length,
+ i = -1;
+ while (++i < n)
+ formats[i][0] = _getInstance(formats[i][0] as String);
+ return (var date) {
+ if (date is num) {
+ date = new DateTime.fromMillisecondsSinceEpoch(date.toInt());
+ }
+ var i = 0,
+ f = formats[i];
+ while (f.length < 2 || f[1](date) == false) {
+ i++;
+ if (i < n) f = formats[i];
+ }
+ if (i == n) return null;
+ return f[0].apply(date);
+ };
+ }
+
+ UTCTimeFormat utc([String specifier = null]) {
+ return new UTCTimeFormat(specifier == null ?
+ _template : specifier, _locale);
+ }
+
+ static UTCTimeFormat iso() {
+ return new UTCTimeFormat("%Y-%m-%dT%H:%M:%S.%LZ");
+ }
+
+ static Map timeFormatPads = {"-": "", "_": " ", "0": "0"};
+ // TODO(songrenchu): Cannot fully be equivalent now.
+ static Map timeFormatsTransform = {
+ 'a': 'EEE',
+ 'A': 'EEEE',
+ 'b': 'MMM',
+ 'B': 'MMMM',
+ 'c': 'EEE MMM d HH:mm:ss yyyy',
+ 'd': 'dd',
+ 'e': 'd', // TODO(songrenchu): zero padding not supported
+ 'H': 'HH',
+ 'I': 'hh',
+ 'j': 'DDD',
+ 'm': 'MM',
+ 'M': 'mm',
+ 'L': 'SSS',
+ 'p': 'a',
+ 'S': 'ss',
+ 'U': 'ww', // TODO(songrenchu): ICU doesn't disdinguish 'U' and 'W',
+ // and not supported by Dart: DateFormat
+ 'w': 'ee', // TODO(songrenchu): e not supported by Dart: DateFormat
+ 'W': 'ww', // TODO(songrenchu): ICU doesn't disdinguish 'U' and 'W',
+ // and not supported by Dart: DateFormat
+ 'x': 'MM/dd/yyyy',
+ 'X': 'HH:mm:ss',
+ 'y': 'yy',
+ 'Y': 'yyyy',
+ 'Z': 'Z',
+ '%': '%'
+ };
+
+ String _wrapStrptime2ICU(String template) {
+ var string = [],
+ i = -1,
+ j = 0,
+ n = template.length,
+ formatPad,
+ tempChar;
+ while (++i < n) {
+ if (template[i] == '%') {
+ string.add(template.substring(j, i));
+ if ((formatPad = timeFormatPads[tempChar = template[++i]]) != null)
+ tempChar = template[++i];
+ if (timeFormatsTransform[tempChar] != null)
+ string.add(timeFormatsTransform[tempChar]);
+ j = i + 1;
+ }
+ }
+ if (j < i)
+ string.add("'" + template.substring(j, i) + "'");
+ return string.join("");
+ }
+}
+
+class UTCTimeFormat extends TimeFormat {
+ UTCTimeFormat(String template, [String identifier = 'en_US']):
+ super(template, identifier);
+
+ UTCTimeFormat _getInstance(String template) {
+ return new UTCTimeFormat(template, _locale);
+ }
+
+ DateTime parse(String string) {
+ assert(_dateFormat != null);
+ return _dateFormat.parseUTC(string);
+ }
+}
\ No newline at end of file
diff --git a/pub/charted/lib/locale/time_scale.dart b/pub/charted/lib/locale/time_scale.dart
new file mode 100644
index 0000000..02a5ad4
--- /dev/null
+++ b/pub/charted/lib/locale/time_scale.dart
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.locale;
+
+class TimeScale extends LinearScale {
+ static List _scaleSteps = [
+ 1e3, // 1-second
+ 5e3, // 5-second
+ 15e3, // 15-second
+ 3e4, // 30-second
+ 6e4, // 1-minute
+ 3e5, // 5-minute
+ 9e5, // 15-minute
+ 18e5, // 30-minute
+ 36e5, // 1-hour
+ 108e5, // 3-hour
+ 216e5, // 6-hour
+ 432e5, // 12-hour
+ 864e5, // 1-day
+ 1728e5, // 2-day
+ 6048e5, // 1-week
+ 2592e6, // 1-month
+ 7776e6, // 3-month
+ 31536e6 // 1-year
+ ];
+
+ static List _scaleLocalMethods = [
+ [chartTime.Time.second, 1],
+ [chartTime.Time.second, 5],
+ [chartTime.Time.second, 15],
+ [chartTime.Time.second, 30],
+ [chartTime.Time.minute, 1],
+ [chartTime.Time.minute, 5],
+ [chartTime.Time.minute, 15],
+ [chartTime.Time.minute, 30],
+ [chartTime.Time.hour, 1],
+ [chartTime.Time.hour, 3],
+ [chartTime.Time.hour, 6],
+ [chartTime.Time.hour, 12],
+ [chartTime.Time.day, 1],
+ [chartTime.Time.day, 2],
+ [chartTime.Time.week, 1],
+ [chartTime.Time.month, 1],
+ [chartTime.Time.month, 3],
+ [chartTime.Time.year, 1]
+ ];
+
+ static TimeFormatFunction _scaleLocalFormat = new TimeFormat().multi([
+ [".%L", (DateTime d) => d.millisecond > 0],
+ [":%S", (DateTime d) => d.second > 0],
+ ["%I:%M", (DateTime d) => d.minute > 0],
+ ["%I %p", (DateTime d) => d.hour > 0],
+ ["%a %d", (DateTime d) => (d.weekday % 7) > 0 && d.day != 1],
+ ["%b %d", (DateTime d) => d.day != 1],
+ ["%B", (DateTime d) => d.month > 1],
+ ["%Y", (d) => true]
+ ]);
+
+ TimeScale([List domain = LinearScale.defaultDomainRange,
+ List range = LinearScale.defaultDomainRange,
+ interpolators.Interpolator interpolator = interpolators.interpolateNumber,
+ bool clamp = false]) : super(domain, range, interpolator, clamp);
+
+ DateTime _timeScaleDate(num t) {
+ return new DateTime.fromMillisecondsSinceEpoch(t);
+ }
+
+ List _tickMethod(Extent extent, int count) {
+ var span = extent.max - extent.min,
+ target = span / count,
+ i = ScaleUtil.bisect(_scaleSteps, target);
+
+ return i == _scaleSteps.length ?
+ [chartTime.Time.year, linearTickRange(
+ [extent.min / 31536e6, extent.max / 31536e6], count)[2]] :
+ i == 0 ? [new ScaleMilliSeconds(),
+ linearTickRange([extent.min, extent.max], count)[2]] :
+ _scaleLocalMethods[target / _scaleSteps[i - 1] <
+ _scaleSteps[i] / target ? i - 1 : i];
+ }
+
+ /**
+ * Given a value x as DateTime or TimeStamp, returns the corresponding value
+ * in the output range.
+ */
+ apply(x){
+ return super.apply(x is DateTime ? x.millisecondsSinceEpoch: x);
+ }
+
+ /**
+ * Returns the value in the input domain x for the corresponding value in the
+ * output range y. This represents the inverse mapping from range to domain.
+ * If elements in range are not number, the invert function returns null.
+ */
+ invert(y) {
+ return super.invert(y);
+ }
+
+ /** Sets the domain of the scale. */
+ set domain(List newDomain) {
+ assert(newDomain.length > 1);
+ super.domain = newDomain.map(
+ (d) => d is DateTime ? d.millisecondsSinceEpoch : d).toList();
+ }
+
+ Function tickFormat(int ticks, [String format = null]) {
+ return _scaleLocalFormat;
+ }
+
+
+ /**
+ * Returns an exact copy of this time scale. Changes to this scale will not
+ * affect the returned scale, and vice versa.
+ **/
+ TimeScale copy() => new TimeScale(domain, range, interpolator, clamp);
+
+ List niceInterval(var interval, [int skip = 1]) {
+ var extent = _scaleDomainExtent();
+
+ var method = interval == null ? _tickMethod(extent, 10) :
+ interval is int ? _tickMethod(extent, interval) : null;
+
+ if (method != null) {
+ interval = method[0];
+ skip = method[1];
+ }
+
+ bool skipped(var date) {
+ if (date is DateTime) date = date.millisecondsSinceEpoch;
+ return (interval as chartTime.Interval)
+ .range(date, date + 1, skip).length == 0;
+ }
+
+ if (skip > 1) {
+ domain = scaleNice(domain,
+ (date) {
+ while (skipped(date = (interval as chartTime.Interval).floor(date))) {
+ date = _timeScaleDate(date.millisecondsSinceEpoch - 1);
+ }
+ return date.millisecondsSinceEpoch;
+ },
+ (date) {
+ while (skipped(date = (interval as chartTime.Interval).ceil(date))) {
+ date = _timeScaleDate(date.millisecondsSinceEpoch + 1);
+ }
+ return date.millisecondsSinceEpoch;
+ }
+ );
+ } else {
+ domain = scaleNice(domain,
+ (date) => interval.floor(date).millisecondsSinceEpoch,
+ (date) => interval.ceil(date).millisecondsSinceEpoch
+ );
+ }
+ return domain;
+ }
+
+ void nice([int ticks]) {
+ domain = niceInterval(ticks);
+ }
+
+ Extent _scaleDomainExtent() {
+ var extent = scaleExtent(domain);
+ return new Extent(extent[0], extent[1]);
+ }
+
+ List ticksInterval(var interval, [int skip = 1]) {
+ var extent = _scaleDomainExtent();
+ var method = interval == null ? _tickMethod(extent, 10) :
+ interval is int ? _tickMethod(extent, interval) :
+ [interval, skip];
+
+ if (method != null) {
+ interval = method[0];
+ skip = method[1];
+ }
+
+ return interval.range(extent.min, extent.max + 1, skip < 1 ? 1 : skip);
+ }
+
+ List ticks([int ticks = 10]) {
+ return ticksInterval(ticks);
+ }
+}
+
+class ScaleMilliSeconds extends chartTime.Interval {
+ DateTime floor(var date) =>
+ date is num ? new DateTime.fromMillisecondsSinceEpoch(date) : date;
+ DateTime ceil(var date) =>
+ date is num ? new DateTime.fromMillisecondsSinceEpoch(date) : date;
+ List range(var t0, var t1, int step) {
+ int start = t0 is DateTime ? t0.millisecondsSinceEpoch : t0,
+ stop = t1 is DateTime ? t1.millisecondsSinceEpoch : t1;
+ return new Range((start / step).ceil() * step, stop, step).map(
+ (d) => new DateTime.fromMillisecondsSinceEpoch(d)).toList();
+ }
+}
diff --git a/pub/charted/lib/scale/linear_scale.dart b/pub/charted/lib/scale/linear_scale.dart
new file mode 100644
index 0000000..2b56769
--- /dev/null
+++ b/pub/charted/lib/scale/linear_scale.dart
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.scale;
+
+class LinearScale extends Scale {
+ Function input;
+ Function output;
+ static const defaultDomainRange = const [0, 1];
+
+ /**
+ * Constructs a new linear scale with the default domain [0,1] and the default
+ * range [0,1]. Thus, the default linear scale is equivalent to the identity
+ * function for numbers; for example linear(0.5) returns 0.5.
+ */
+ LinearScale([List domain = defaultDomainRange,
+ List range = defaultDomainRange,
+ interpolators.Interpolator interpolator = interpolators.interpolateNumber,
+ bool clamp = false]) {
+ _initializeScale(domain, range, interpolator, clamp);
+ }
+
+ _initializeScale(List domain, List range,
+ interpolators.Interpolator interpolator, bool clamp) {
+ _domain = domain;
+ _range = range;
+ _interpolator = interpolator;
+ _clamp = clamp;
+
+ Function linear = math.min(domain.length, range.length) > 2 ?
+ ScaleUtil.polylinearScale: ScaleUtil.bilinearScale;
+ Function uninterpolator = clamp ? interpolators.uninterpolateClamp :
+ interpolators.uninterpolateNumber;
+ if (range[0] is num) {
+ input = linear(range, domain, uninterpolator,
+ interpolators.interpolateNumber);
+ }
+ output = linear(domain, range, uninterpolator, interpolator);
+ }
+
+ /**
+ * Given a value x in the input domain, returns the corresponding value in
+ * the output range.
+ */
+ apply(x) {
+ _initializeScale(_domain, _range, _interpolator, _clamp);
+ return output(x);
+ }
+
+ /**
+ * Returns the value in the input domain x for the corresponding value in the
+ * output range y. This represents the inverse mapping from range to domain.
+ * If elements in range are not number, the invert function returns null.
+ */
+ invert(y) {
+ _initializeScale(_domain, _range, _interpolator, _clamp);
+ return input != null ? input(y) : null;
+ }
+
+ /** Sets the domain of the scale. */
+ set domain(List newDomain) {
+ _domain = newDomain;
+ }
+
+ get domain => _domain;
+
+ /** Sets the range of the scale. */
+ set range(List newRange) {
+ _range = newRange;
+ }
+
+ get range => _range;
+
+ /**
+ * Sets the scale's output range to the specified array of values, while also
+ * setting the scale's interpolator to d3.interpolateRound. This is a
+ * convenience routine for when the values output by the scale should be
+ * exact integers, such as to avoid antialiasing artifacts. It is also
+ * possible to round the output values manually after the scale is applied.
+ */
+ rangeRound(List newRange) {
+ _initializeScale(_domain, newRange, interpolators.interpolateRound, _clamp);
+ }
+
+ /**
+ * Enables or disables clamping accordingly. By default, clamping is
+ * disabled, such that if a value outside the input domain is passed to the
+ * scale, the scale may return a value outside the output range through linear
+ * extrapolation.
+ */
+ set clamp(bool clamp) {
+ _clamp = clamp;
+ }
+
+ get clamp => _clamp;
+
+ /**
+ * Sets the interpolator of the scale. If it's not set, the scale will try to
+ * find the correct interpolator base on the domain and range input.
+ */
+ set interpolator(interpolators.Interpolator newInterpolator) {
+ _interpolator = newInterpolator;
+ }
+
+ get interpolator => _interpolator;
+
+ /** Sets the amount of ticks in the scale, default is 10. */
+ List ticks([int ticks = 10]) {
+ return _linearTicks(domain, ticks);
+ }
+
+ Function tickFormat(int ticks, [String format = null]) {
+ return _linearTickFormat(_domain, ticks, format);
+ }
+
+ _linearTickFormat(List domain, int ticks, String format) {
+ var tickRange = _linearTickRange(domain, ticks);
+ return new EnusLocale().numberFormat.format((format != null) ?
+ format : ",." + _linearPrecision(tickRange[2]).toString() + "f");
+ }
+
+ // Returns the number of significant digits after the decimal point.
+ int _linearPrecision(value) {
+ return -(math.log(value) / math.LN10 + .01).floor();
+ }
+
+ /**
+ * Extends the domain so that it starts and ends on nice round values.
+ * The optional tick count argument allows greater control over the step size
+ * used to extend the bounds, guaranteeing that the returned ticks will
+ * exactly cover the domain.
+ **/
+ void nice([int ticks = 10]) {
+ _domain = _linearNice(_domain, ticks);
+ }
+
+ /**
+ * Returns an exact copy of this linear scale. Changes to this scale will not
+ * affect the returned scale, and vice versa.
+ **/
+ LinearScale copy() => new LinearScale(_domain, _range, _interpolator, _clamp);
+
+ List _linearNice(List domain, [int ticks = 10]) {
+ return ScaleUtil.nice(domain,
+ ScaleUtil.niceStep(_linearTickRange(domain, ticks)[2]));
+ }
+
+ List _linearTicks(List domain, int ticks) {
+ List args = _linearTickRange(domain, ticks);
+ return new Range(args[0], args[1], args[2]).toList();
+ }
+
+ List _linearTickRange(List domain, int ticks) {
+ var extent = scaleExtent(domain),
+ span = extent[1] - extent[0],
+ step = math.pow(10, (math.log(span / ticks) / math.LN10).floor()),
+ err = ticks / span * step;
+
+ // Filter ticks to get closer to the desired count.
+ if (err <= .15) step *= 10;
+ else if (err <= .35) step *= 5;
+ else if (err <= .75) step *= 2;
+
+ List tickRange = new List(3);
+ // Round start and stop values to step interval.
+ tickRange[0] = (extent[0] / step).ceil() * step;
+ tickRange[1] = (extent[1] / step).floor() * step + step * .5; // inclusive
+ tickRange[2] = step;
+ return tickRange;
+ }
+
+ get linearTickRange => _linearTickRange;
+}
diff --git a/pub/charted/lib/scale/log_scale.dart b/pub/charted/lib/scale/log_scale.dart
new file mode 100644
index 0000000..7d1107c
--- /dev/null
+++ b/pub/charted/lib/scale/log_scale.dart
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.scale;
+
+// TODO(midoringo): Write more test for this class.
+/**
+ * Log scales are similar to linear scales, except there's a logarithmic
+ * transform that is applied to the input domain value before the output range
+ * value is computed. The mapping to the output range value y can be expressed
+ * as a function of the input domain value x: y = m log(x) + b.
+ *
+ * As log(0) is negative infinity, a log scale must have either an
+ * exclusively-positive or exclusively-negative domain; the domain must not
+ * include or cross zero. A log scale with a positive domain has a well-defined
+ * behavior for positive values, and a log scale with a negative domain has a
+ * well-defined behavior for negative values (the input value is multiplied by
+ * -1, and the resulting output value is also multiplied by -1). The behavior of
+ * the scale is undefined if you pass a negative value to a log scale with a
+ * positive domain or vice versa.
+ */
+class LogScale extends Scale {
+ Function input;
+ Function output;
+ static const defaultBase = 10;
+ static const defaultDomain = const [1, 10];
+
+ LinearScale _linear;
+ int _base;
+ bool _positive;
+ List _domain;
+
+ LogScale([LinearScale linear = null,
+ this._base = defaultBase,
+ this._positive = true,
+ this._domain = defaultDomain]) {
+ _linear = (linear != null) ? linear : new LinearScale(_domain);
+ }
+
+ num _log(x) => (_positive ? math.log(x < 0 ? 0 : x) :
+ -math.log(x > 0 ? 0 : -x)) / math.log(_base);
+
+ _pow(x) => _positive ? math.pow(_base, x) : -math.pow(_base, -x);
+
+ /**
+ * Given a value x in the input domain, returns the corresponding value in
+ * the output range.
+ */
+ apply(x) => _linear.apply(_log(x));
+
+ /**
+ * Returns the value in the input domain x for the corresponding value in the
+ * output range y. This represents the inverse mapping from range to domain.
+ * For a valid value y in the output range, log(log.invert(y)) equals y;
+ * similarly, for a valid value x in the input domain, log.invert(log(x))
+ * equals x. Equivalently, you can construct the invert operator by building
+ * a new scale while swapping the domain and range. The invert operator is
+ * particularly useful for interaction, say to determine the value in the
+ * input domain that corresponds to the pixel location under the mouse.
+ */
+ invert(x) => _pow(_linear.invert(x));
+
+ /** Sets the domain of the scale. */
+ set domain(List x) {
+ _positive = x[0] >= 0;
+ _domain = x;
+ _linear.domain = _domain.map((e) => _log(e)).toList();
+ }
+ get domain => _domain;
+
+ /** Sets the base of the logarithmic scale. */
+ get base => _base;
+ set base(int newBase) {
+ this._base = newBase;
+ _linear.domain = _domain.map((e) => _log(e)).toList();
+ }
+
+ /** Sets the range of the scale. */
+ get range => _linear.range;
+ set range(List newRange) {
+ _linear.range = newRange;
+ }
+
+ /**
+ * Sets the scale's output range to the specified array of values, while also
+ * setting the scale's interpolator to d3.interpolateRound. This is a
+ * convenience routine for when the values output by the scale should be
+ * exact integers, such as to avoid antialiasing artifacts. It is also
+ * possible to round the output values manually after the scale is applied.
+ */
+ void rangeRound(List newRange) {
+ _linear.rangeRound(newRange);
+ }
+
+ /** Sets the interpolator used in the scale. */
+ set interpolator(interpolators.Interpolator newInterpolator) {
+ _linear.interpolator = newInterpolator;
+ }
+
+ get interpolator => _linear.interpolator;
+
+ /**
+ * Enables or disables clamping accordingly. By default, clamping is
+ * disabled, such that if a value outside the input domain is passed to the
+ * scale, the scale may return a value outside the output range through linear
+ * extrapolation.
+ */
+ set clamp(bool clamp) {
+ _linear.clamp = clamp;
+ }
+
+ get clamp => _linear.clamp;
+
+ /**
+ * Extends the domain so that it starts and ends on nice round values.
+ * The optional tick count argument allows greater control over the step size
+ * used to extend the bounds, guaranteeing that the returned ticks will
+ * exactly cover the domain.
+ **/
+ nice([int ticks]) {
+ var niced;
+ if (_positive) {
+ niced = scaleNice(_domain.map((e) => _log(e)).toList());
+ } else {
+ var floor = (x) => -(-x).ceil();
+ var ceil = (x) => -(-x).floor();
+ niced = scaleNice(_domain.map((e) => _log(e)).toList(), floor, ceil);
+ }
+ _linear.domain = niced;
+ domain = niced.map((e) => _pow(e)).toList();
+ }
+
+ /**
+ * Returns representative values from the scale's input domain. The returned
+ * tick values are uniformly spaced within each power of ten,
+ * and are guaranteed to be within the extent of the input domain.
+ */
+ List ticks([int ticks = 10]) {
+ var extent = scaleExtent(_domain),
+ ticks = [],
+ u = extent[0],
+ v = extent[1],
+ i = (_log(u)).floor(),
+ j = (_log(v)).ceil(),
+ n = (_base % 1 > 0) ? 2 : _base;
+
+ if ((j - i).isFinite) {
+ if (_positive) {
+ for (; i < j; i++) for (var k = 1; k < n; k++) ticks.add(_pow(i) * k);
+ ticks.add(_pow(i));
+ } else {
+ ticks.add(_pow(i));
+ for (; i++ < j;) for (var k = n - 1; k > 0; k--) ticks.add(_pow(i) * k);
+ }
+ for (i = 0; ticks[i] < u; i++) {} // strip small values
+ for (j = ticks.length; ticks[j - 1] > v; j--) {} // strip big values
+ ticks = ticks.sublist(i, j);
+ }
+ return ticks;
+ }
+
+ /**
+ * Returns a number format function suitable for displaying a tick value.
+ * The returned tick format is implemented as d.toPrecision(1)
+ */
+ Function tickFormat(int ticks, [String formatString = null]) {
+ var logFormatFunction = formatString != null ?
+ format(formatString) : format(".0e");
+ var k = math.max(.1, ticks / this.ticks().length),
+ e = _positive ? 1e-12 : -1e-12;
+ return (d) {
+ if (_positive) {
+ return d / _pow((_log(d) + e).ceil()) <= k ? logFormatFunction(d) : '';
+ } else {
+ return d / _pow((_log(d) + e).floor()) <= k ? logFormatFunction(d) : '';
+ }
+ };
+ }
+
+ copy() {
+ return new LogScale(_linear.copy(), _base, _positive, _domain);
+ }
+}
diff --git a/pub/charted/lib/scale/ordinal_scale.dart b/pub/charted/lib/scale/ordinal_scale.dart
new file mode 100644
index 0000000..f1e86c9
--- /dev/null
+++ b/pub/charted/lib/scale/ordinal_scale.dart
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.scale;
+
+class OrdinalScale extends Scale {
+ Map index = new Map();
+ Function _rangeSetupFunction;
+ List _rangeSetupArgs = [[]];
+
+ /**
+ * Constructs a new ordinal scale with an empty domain and an empty range.
+ * The ordinal scale is invalid (always returning undefined) until an output
+ * range is specified.
+ */
+ OrdinalScale() {
+ _rangeSetupFunction = defaultRange;
+ domain = [];
+ }
+
+ /**
+ * Given a value x in the input domain, returns the corresponding value in
+ * the output range.
+ */
+ apply(input) {
+ if (index[input] == null) {
+ index[input] = _domain.length;
+ _domain.add(input);
+ }
+ return range[(index[input]) % range.length];
+ }
+
+ List steps(start, step) {
+ var s = new Range(domain.length).toList();
+ return s.map((num i) => start + step * i).toList();
+ }
+
+ void nice([int ticks]) {
+ // NO-OP for an ordinal scale.
+ }
+
+ /**
+ * Sets the domain Value. Setting the domain on an ordinal scale is optional.
+ * If no domain is set, a range must be set explicitly. Then, each unique
+ * value that is passed to the scale function will be assigned a new value
+ * from the output range.
+ */
+ // TODO(midoringo): It would be nice to be able to match both D3 and dart
+ // pattern here. In D3, if newDomain is not defined, this returns the current
+ // domain. Here we use the Dart syntax for setting and getting domain.
+ set domain(List newDomain) {
+ _domain = [];
+ index = new Map();
+ int i = -1;
+ int n = newDomain.length;
+ var xi;
+ while (++i < n) {
+ xi = newDomain[i];
+ if (index[xi] == null) {
+ index[xi] = _domain.length;
+ _domain.add(xi);
+ }
+ }
+ Function.apply(_rangeSetupFunction, _rangeSetupArgs);
+ }
+
+ get domain => _domain;
+
+ /**
+ * Sets the output range of the ordinal scale to the specified array of
+ * values. The first element in the domain will be mapped to the first element
+ * in values, the second domain value to the second range value, and so on. If
+ * there are fewer elements in the range than in the domain, the scale will
+ * recycle values from the start of the range.
+ */
+ set range(List newRange) {
+ _range = newRange;
+ rangeBand = 0;
+ _rangeSetupFunction = defaultRange;
+ _rangeSetupArgs = [newRange];
+ }
+
+ get range => _range;
+
+ defaultRange(List newRange) {
+ range = newRange;
+ }
+
+ /**
+ * Sets the output range from the specified continuous interval. The array
+ * interval contains two elements representing the minimum and maximum numeric
+ * value. This interval is subdivided into n evenly-spaced points, where n is
+ * the number of (unique) values in the input domain. The first and last point
+ * may be offset from the edge of the interval according to the specified
+ * padding, which defaults to zero. The padding is expressed as a multiple of
+ * the spacing between points. A reasonable value is 1.0, such that the first
+ * and last point will be offset from the minimum and maximum value by half
+ * the distance between points.
+ */
+ void rangePoints(List x, [double padding = 0.0]) {
+ var start = x[0];
+ var stop = x[1];
+ var step = (stop - start) / (domain.length - 1 + padding);
+ range = steps(domain.length < 2 ?
+ (start + stop) / 2 : start + step * padding / 2, step);
+ rangeBand = 0;
+ _rangeSetupFunction = rangePoints;
+ _rangeSetupArgs = [x, padding];
+ }
+
+ /**
+ * Sets the output range from the specified continuous interval. The array
+ * interval contains two elements representing the minimum and maximum numeric
+ * value. This interval is subdivided into n evenly-spaced bands, where n is
+ * the number of (unique) values in the input domain. The bands may be offset
+ * from the edge of the interval and other bands according to the specified
+ * padding, which defaults to zero. The padding is typically in the range
+ * [0,1] and corresponds to the amount of space in the range interval to
+ * allocate to padding. A value of 0.5 means that the band width will be
+ * equal to the padding width. The outerPadding argument is for the entire
+ * group of bands; a value of 0 means there will be padding only between
+ * rangeBands.
+ */
+ void rangeBands(List x, [double padding = 0.0, double outerPadding]) {
+ if (outerPadding == null) outerPadding = padding;
+
+ var reverse = x[1] < x[0] ? 1 : 0,
+ start = x[reverse - 0],
+ stop = x[1 - reverse],
+ step = (stop - start) / (domain.length - padding + 2 * outerPadding);
+
+ range = steps(start + step * outerPadding, step);
+ if (reverse > 0) range = _range.reversed.toList();
+ rangeBand = step * (1 - padding);
+ _rangeSetupFunction = rangeBands;
+ _rangeSetupArgs = [x, padding];
+ }
+
+ /**
+ * Like rangeBands, except guarantees that the band width and offset are
+ * integer values, so as to avoid antialiasing artifact
+ */
+ void rangeRoundBands(List x, [double padding = 0.0, double outerPadding]) {
+ if (outerPadding == null) outerPadding = padding;
+
+ var reverse = x[1] < x[0] ? 1 : 0,
+ start = x[reverse - 0],
+ stop = x[1 - reverse],
+ step = ((stop - start) /
+ (domain.length - padding + 2 * outerPadding)).floor(),
+ error = stop - start - (domain.length - padding) * step;
+
+ range = steps(start + (error / 2).round(), step);
+ if (reverse > 0) range = _range.reversed.toList();
+ rangeBand = (step * (1 - padding)).round();
+ _rangeSetupFunction = rangeRoundBands;
+ _rangeSetupArgs = [x, padding];
+ }
+
+ List rangeExtent() {
+ return scaleExtent(_rangeSetupArgs[0]);
+ }
+
+ Scale copy() {
+ return new OrdinalScale()
+ ..domain = _domain
+ ..range = _range
+ ..rangeBand = rangeBand
+ .._rangeSetupFunction = _rangeSetupFunction
+ .._rangeSetupArgs = _rangeSetupArgs;
+ }
+
+ /**
+ * Returns the value in input domain x for the corresponding value in the
+ * output range y. In Ordinal scale, the output are String values and are not
+ * interpolated, so y must match an element in the output range. Null is
+ * returned if the specified y is not an element in range or if the index of
+ * the element in range is out of bound of domain's length.
+ */
+ invert(y) {
+ var valueIndex = _range.indexOf(y);
+ if (valueIndex > -1 && valueIndex < _domain.length) {
+ return _domain[_range.indexOf(y)];
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/pub/charted/lib/scale/scale.dart b/pub/charted/lib/scale/scale.dart
new file mode 100644
index 0000000..2d9d462
--- /dev/null
+++ b/pub/charted/lib/scale/scale.dart
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+/*
+ * TODO(midoringo): Document library
+ */
+library charted.scale;
+
+import 'dart:math' as math;
+import 'package:charted/core/core.dart';
+import 'package:charted/format/format.dart';
+import 'package:charted/locale/locale.dart';
+import 'package:charted/interpolators/interpolators.dart' as interpolators;
+
+part 'ordinal_scale.dart';
+part 'linear_scale.dart';
+part 'log_scale.dart';
+
+/** Minimum common interface to be supported by all scales */
+abstract class Scale {
+ var rangeBand = 0;
+ List _domain;
+ List _range;
+ interpolators.Interpolator _interpolator;
+ bool _clamp;
+
+
+ /**
+ * Returns the extent of the scale as a list containing the min and max of the
+ * domain.
+ */
+ List scaleExtent(List domain) {
+ var start = domain[0];
+ var stop = domain[domain.length - 1];
+ return start < stop ? [start, stop] : [stop, start];
+ }
+
+ /**
+ * Returns the extent of the domain in nice round values. If alternative
+ * floor and ceil functions are not provided, default floor and ceil will be
+ * used to produce the round values.
+ */
+ scaleNice(List domain, [altFloor = null, altCeil = null]) {
+ var i0 = 0;
+ var i1 = domain.length -1;
+ var x0 = domain[i0];
+ var x1 = domain[i1];
+ var dx;
+ if (x1 < x0) {
+ dx = i0;
+ i0 = i1;
+ i1 = dx;
+ dx = x0;
+ x0 = x1;
+ x1 = dx;
+ }
+
+ domain[i0] = (altFloor != null) ? altFloor(x0) : x0.floor();
+ domain[i1] = (altCeil != null) ? altCeil(x1) : x1.ceil();
+ return domain;
+ }
+
+ void nice([int ticks = 10]);
+
+ /**
+ * Returns the values in the domain as tick values for sub class that doesn't
+ * implement the ticks method.
+ **/
+ List ticks([int ticks = 10]) => _domain;
+
+ /**
+ * Returns the identity function as the tickformat Function for sub class that
+ * doesn't implment the tickFormat method.
+ */
+ Function tickFormat(int ticks, [String format = null]) => identityFunction;
+
+ /**
+ * Returns a two-element array representing the extent of the scale's range,
+ * i.e., the smallest and largest values.
+ */
+ List rangeExtent() => scaleExtent(_range);
+
+ set domain(List domain);
+ get domain => _domain;
+
+ set range(List range);
+ get range => _range;
+
+ apply(x);
+ Scale copy();
+}
diff --git a/pub/charted/lib/selection/selection.dart b/pub/charted/lib/selection/selection.dart
new file mode 100644
index 0000000..4c6f1db
--- /dev/null
+++ b/pub/charted/lib/selection/selection.dart
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+/*
+ * TODO(prsd): Document library
+ */
+library charted.selection;
+
+import "dart:html" show Element, Event, document;
+import "dart:math" as math;
+import "package:charted/core/core.dart";
+import "package:charted/transition/transition.dart";
+
+part "selection_scope.dart";
+part "selection_impl.dart";
+
+/**
+ * Callback to access key value from a given data object. During the process
+ * of binding data to Elements, the key values are used to match Elements
+ * that have previously bound data
+ */
+typedef SelectionKeyFunction(datum);
+
+/**
+ * Callback for all DOM related operations - The first parameter [datum] is
+ * the piece of data associated with the node, [ei] is the index of the
+ * element in it's group and [c] is the context to which the data is
+ * associated to.
+ */
+typedef E SelectionCallback<E>(datum, int index, Element element);
+
+/** Callback used to access a value from a datum */
+typedef E SelectionValueAccessor<E>(datum, int index);
+
+/** Create a ChartedCallback that always returns [val] */
+SelectionCallback toCallback(val) => (datum, index, element) => val;
+
+/** Create a ChartedValueAccessor that always returns [val] */
+SelectionValueAccessor toValueAccessor(val) => (datum, index) => val;
+
+/**
+ * [Selection] is a collection of elements - this collection defines
+ * operators that can be applied on all elements of the collection.
+ *
+ * All operators accept parameters either as a constant value or a callback
+ * function (typically using the named parameters "val" and "fn"). These
+ * operators, when invoked with the callback function, the function is
+ * called once per element and is passed the "data" associated, the "index"
+ * and the element itself.
+ */
+abstract class Selection {
+ /**
+ * Collection of groups - A selection when created by calling [selectAll]
+ * on an existing [Selection], could contain more than one group.
+ */
+ Iterable<SelectionGroup> groups;
+
+ /**
+ * Scope of this selection that manages the element, data associations for
+ * all elements in this selection (and the sub-selections)
+ */
+ SelectionScope get scope;
+
+ /** Indicates if this selection is empty */
+ bool get isEmpty;
+
+ /** Number of elements in this selection */
+ int get length;
+
+ /** First non-null element in this selection, if any. */
+ Element get first;
+
+ /**
+ * Creates and returns a new [Selection] containing the first element
+ * matching [selector] under each element in the current selection.
+ *
+ * If an element does not have a matching decendant, a placeholder is used
+ * in it's position - thus being able to match the indices of elements in
+ * the current and the created sub-selection.
+ *
+ * Any data bound to elements in this selection is inherited by the
+ * selected decendants.
+ */
+ Selection select(String selector);
+
+ /**
+ * Same as [select], but calls [fn] for each non-null element in the
+ * selection (with data associated to the element, index of the element in
+ * it's group and the element itself) to get the selected element that will
+ * be selected.
+ */
+ Selection selectWithCallback(SelectionCallback<Element> fn);
+
+ /**
+ * Creates and returns a new [Selection] containing all elements matching
+ * [selector] under each element in the current selection.
+ *
+ * The resulting [Selection] is nested with elements from current selection
+ * as parents and the selected decendants grouped by elements in the
+ * current selection. When no decendants match the selector, the
+ * collection of selected elements in a group is empty.
+ *
+ * Data bound to the elements is not automatically inherited by the
+ * selected decendants.
+ */
+ Selection selectAll(String selector);
+
+ /**
+ * Same as [selectAll], but calls [fn] for each non-null element in the
+ * selection (with data associated to the element, index of the element in
+ * it's group and the element itself) to get a collection of selected
+ * elements that will be part of the new selection.
+ */
+ Selection selectAllWithCallback(SelectionCallback<Iterable<Element>> fn);
+
+ /**
+ * Sets the attribute [name] on all elements when [val] is not null.
+ * Removes the attribute when [val] is null.
+ */
+ void attr(String name, val);
+
+ /**
+ * Same as [attr], but calls [fn] for each non-null element in the
+ * selection (with data associated to the element, index of the element in
+ * it's group and the element itself) to get the value of the attribute.
+ */
+ void attrWithCallback(String name, SelectionCallback fn);
+
+ /**
+ * Ensures presence of a class when [val] is true. Ensures that the class
+ * isn't present if [val] is false.
+ */
+ void classed(String name, [bool val = true]);
+
+ /**
+ * Same as [classed], but calls [fn] for each non-null element in the
+ * selection (with data associated to the element, index of the element in
+ * it's group and the element itself) to get the boolean value that
+ * indicates if the class must be added or removed.
+ */
+ void classedWithCallback(String name, SelectionCallback<bool> fn);
+
+ /** Sets CSS [property] to [val] on all elements in the selection. */
+ void style(String property, val, {String priority});
+
+ /**
+ * Same as [style], but calls [fn] for each non-null element in the
+ * selection (with data associated to the element, index of the element in
+ * it's group and the element itself) to get the value of the property.
+ */
+ void styleWithCallback(String property,
+ SelectionCallback<String> fn, {String priority});
+
+ /**
+ * Sets textContent of all elements in the selection to [val]. A side-effect
+ * of this call is that any children of these elements will not be part of
+ * the DOM anymore.
+ */
+ void text(String val);
+
+ /**
+ * Same as [text], but calls [fn] for each non-null element in
+ * the selection (with data associated to the element, index of the
+ * element in it's group and the element itself) to get the text content
+ */
+ void textWithCallback(SelectionCallback<String> fn);
+
+ /**
+ * Sets innerHtml of all elements in the selection to [val]. A side-effect
+ * of this call is that any children of these elements will not be part of
+ * the DOM anymore.
+ */
+ void html(String val);
+
+ /**
+ * Same as [html], but calls [fn] for each non-null element in
+ * the selection (with data associated to the element, index of the
+ * element in it's group and the element itself) to get the html content
+ */
+ void htmlWithCallback(SelectionCallback<String> fn);
+
+ /**
+ * Appends a new child element to each element in the selection.
+ *
+ * Returns a [Selection] containing the newly created elements. As with
+ * [select], any data bound to the elements in this selection is inherited
+ * by the new elements.
+ */
+ Selection append(String tag);
+
+ /**
+ * Same as [append], but calls [fn] for each non-null element in the
+ * selection (with data associated to the element, index of the element in
+ * it's group and the element itself) to get the element to be appended.
+ */
+ Selection appendWithCallback(SelectionCallback<Element> fn);
+
+ /**
+ * Inserts a child node to each element in the selection before the first
+ * element matching [before] or before the element returned by [beforeFn].
+ *
+ * Returns a [Selection] containing the newly created elements. As with
+ * [select], any data bound to the elements in this selection is inherited
+ * by the new elements.
+ */
+ Selection insert(String tag,
+ {String before, SelectionCallback<Element> beforeFn});
+
+ /**
+ * Same as [insert], but calls [fn] for each non-null element in the
+ * selection (with data associated to the element, index of the element in
+ * it's group and the element itself) to get the element to be inserted.
+ */
+ Selection insertWithCallback(SelectionCallback<Element> fn,
+ {String before, SelectionCallback<Element> beforeFn});
+
+ /** Removes all selected elements from the DOM */
+ void remove();
+
+ /** Calls [fn] on each element in this selection */
+ void each(SelectionCallback fn);
+
+ /**
+ * Adds or removes an event [listener] to each element in the selection for
+ * the specified [type] (Eg: "mouseclick", "mousedown")
+ *
+ * Any existing listener of the same type will be removed. To register
+ * multiple listener for the same event type, the [type] can be suffixed
+ * with a namespace. (Eg: "mouseclick.foo", "mousedown.bar")
+ *
+ * When [listener] is null, any existing listener of the same type and in
+ * the same namespace will be removed (Eg: Using "mouseclick.foo" as type
+ * will only remove listeners for "mouseclick.foo" and not "mouseclick.bar")
+ *
+ * To remove listeners of an event type in all namespaces, prefix the type
+ * with a "." (Eg: ".mouseclick" will remove "mouseclick.bar",
+ * "mouseclick .foo" and all other mouseclick event listeners)
+ *
+ * To summarize, [type] can be any DOM event type optionally in the format
+ * "event.namespace" where event is the DOM event type and namespace is
+ * used to distinguish between added listeners.
+ *
+ * When [listener] is called, it is passed the current value associated with
+ * the element. Please note that index passed to the listener contains a
+ * value as it was at the time of adding the listener.
+ */
+ void on(String type, [SelectionCallback listener, bool capture]);
+
+ /**
+ * Associates data with the selected elements.
+ * Computes the enter, update and exit selections.
+ */
+ DataSelection data(Iterable vals, [SelectionKeyFunction keyFn]);
+
+ /**
+ * Same as [data], but calls [fn] for each non-null element in the
+ * selection (with data associated to the element, index of the element in
+ * it's group and the element itself) to get the data to be set on the
+ * current element.
+ */
+ DataSelection dataWithCallback(
+ SelectionCallback<Iterable> fn, [SelectionKeyFunction keyFn]);
+
+ /**
+ * Associates data with all the elements - no join is performed. Unlike
+ * [data], this does not compute the enter, update and exit selections.
+ */
+ void datum(Iterable vals);
+
+ /**
+ * Same as [datum], but calls [fn] for each non-null element in the
+ * selection (with data associated to the element, index of the element in
+ * it's group and the element itself) to get the data to be set on the
+ * current element.
+ */
+ void datumWithCallback(SelectionCallback<Iterable> fn);
+
+ /**
+ * Starts a transition for the current selection. Transitions behave much
+ * like selections, except operators animate smoothly over time rather than
+ * applying instantaneously.
+ */
+ Transition transition();
+}
+
+
+/*
+ * Group of elements in the selection.
+ * Each selection may contain more than one group of elements.
+ */
+abstract class SelectionGroup {
+ Iterable<Element> elements;
+ Element parent;
+}
+
+
+/**
+ * [EnterSelection] is a sub-selection that represents missing elements of a
+ * selection - an element is considered missing when there is data and no
+ * corresponding element in a selection.
+ */
+abstract class EnterSelection {
+ /**
+ * Indicate if this selection is empty
+ * See [Selection.isEmpty] for more information.
+ */
+ bool get isEmpty;
+
+ /** [DataSelection] that corresponds to this selection. */
+ DataSelection get update;
+
+ /**
+ * Appends an element to all elements in this selection and return
+ * [Selection] containing the newly added elements.
+ *
+ * See [Selection.append] for more information.
+ * The new nodes are merged into the [DataSelection]
+ */
+ Selection append(String tag);
+
+ /**
+ * Same as [append] but calls [fn] to get the element to be appended.
+ * See [Selection.appendWithCallback] for more information.
+ */
+ Selection appendWithCallback(SelectionCallback<Element> fn);
+
+ /**
+ * Insert a child node to each element in the selection and return
+ * [Selection] containing the newly added elements.
+ *
+ * See [Selection.insert] for more information.
+ * The new nodes are merged into the [UpdateSelection]
+ */
+ Selection insert(String tag,
+ {String before, SelectionCallback<Element> beforeFn});
+
+ /**
+ * Same as [insert] but calls [fn] to get the element to be inserted.
+ * See [Selection.insertWithCallback] for more information.
+ */
+ Selection insertWithCallback(SelectionCallback<Element> fn,
+ {String before, SelectionCallback<Element> beforeFn});
+
+ /**
+ * For each element in the current selection, select exactly one
+ * decendant and return [Selection] containing the selected elements.
+ *
+ * See [Selection.select] for more information.
+ */
+ Selection select(String selector);
+
+ /**
+ * Same as [select] but calls [fn] to get the element to be inserted.
+ * See [Selection.selectWithCallback] for more information.
+ */
+ Selection selectWithCallback(SelectionCallback<Element> fn);
+}
+
+/*
+ * [ExitSelection] is a sub-selection that represents elements that don't
+ * have data associated to them.
+ */
+abstract class ExitSelection extends Selection {
+ DataSelection get update;
+}
+
+/*
+ * Selection that consists elements in the selection that aren't part of
+ * [EnterSelection] or the [ExitSelection]
+ *
+ * An [UpdateSelection] is only available after data() is attached and is
+ * currently exactly the same as [Selection] itself.
+ */
+abstract class DataSelection extends Selection {
+ /**
+ * A view of the current selection that contains a collection of data
+ * elements which weren't associated with an element in the DOM.
+ */
+ EnterSelection get enter;
+
+ /**
+ * A view of the current selection containing elements that don't have data
+ * associated with them.
+ */
+ ExitSelection get exit;
+}
diff --git a/pub/charted/lib/selection/selection_impl.dart b/pub/charted/lib/selection/selection_impl.dart
new file mode 100644
index 0000000..850ab45
--- /dev/null
+++ b/pub/charted/lib/selection/selection_impl.dart
@@ -0,0 +1,548 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.selection;
+
+/**
+ * Implementation of [Selection].
+ * Selections cannot be created directly - they are only created using
+ * the select or selectAll methods on [SelectionScope] and [Selection].
+ */
+class _SelectionImpl implements Selection {
+
+ Iterable<SelectionGroup> groups;
+ SelectionScope scope;
+
+ Transition _transition;
+
+ /**
+ * Creates a new selection.
+ *
+ * When [source] is not specified, the new selection would have exactly
+ * one group with [SelectionScope.root] as it's parent. Otherwise, one group
+ * per for each non-null element is created with element as it's parent.
+ *
+ * When [selector] is specified, each group contains all elements matching
+ * [selector] and under the group's parent element. Otherwise, [fn] is
+ * called once per group with parent element's "data", "index" and the
+ * "element" itself passed as parameters. [fn] must return an iterable of
+ * elements to be used in each group.
+ */
+ _SelectionImpl.all({String selector, SelectionCallback<Iterable<Element>> fn,
+ SelectionScope this.scope, Selection source}) {
+ assert(selector != null || fn != null);
+ assert(source != null || scope != null);
+
+ if (selector != null) {
+ fn = (d, i, c) => c == null ?
+ scope.root.querySelectorAll(selector) :
+ c.querySelectorAll(selector);
+ }
+
+ var tmpGroups = new List<SelectionGroup>(),
+ index = 0;
+ if (source != null) {
+ scope = source.scope;
+ source.groups.forEach((g) {
+ g.elements.forEach((e) {
+ if (e != null) {
+ tmpGroups.add(
+ new _SelectionGroupImpl(
+ fn(scope.datum(e), index, e), parent: e));
+ }
+ index++;
+ });
+ });
+ } else {
+ tmpGroups.add(
+ new _SelectionGroupImpl(fn(null, 0, null), parent: scope.root));
+ }
+ groups = tmpGroups;
+ }
+
+ /**
+ * Same as [all] but only uses the first element matching [selector] when
+ * [selector] is speficied. Otherwise, call [fn] which must return the
+ * element to be selected.
+ */
+ _SelectionImpl.single({String selector, SelectionCallback<Element> fn,
+ SelectionScope this.scope, Selection source}) {
+ assert(selector != null || fn != null);
+ assert(source != null || scope != null);
+
+ if (selector != null) {
+ fn = (d, i, c) => c == null ?
+ scope.root.querySelector(selector) :
+ c.querySelector(selector);
+ }
+
+ if (source != null) {
+ scope = source.scope;
+ groups = new List<SelectionGroup>.generate(source.groups.length, (gi) {
+ SelectionGroup g = source.groups.elementAt(gi);
+ return new _SelectionGroupImpl(
+ new List.generate(g.elements.length, (ei) {
+ var e = g.elements.elementAt(ei);
+ if (e != null) {
+ var datum = scope.datum(e);
+ var enterElement = fn(datum, ei, e);
+ if (datum != null) {
+ scope.associate(enterElement, datum);
+ }
+ return enterElement;
+ } else {
+ return null;
+ }
+ }), parent: g.parent);
+ });
+ } else {
+ groups = new List<SelectionGroup>.generate(1,
+ (_) => new _SelectionGroupImpl(new List.generate(1,
+ (_) => fn(null, 0, null), growable: false)), growable: false);
+ }
+ }
+
+ /** Creates a selection using the pre-computed list of [SelectionGroup] */
+ _SelectionImpl.selectionGroups(
+ Iterable<SelectionGroup> this.groups, SelectionScope this.scope);
+
+ /**
+ * Creates a selection using the list of elements. All elements will
+ * be part of the same group, with [SelectionScope.root] as the group's parent
+ */
+ _SelectionImpl.elements(Iterable elements, SelectionScope this.scope) {
+ groups = new List<SelectionGroup>()..add(new _SelectionGroupImpl(elements));
+ }
+
+ /**
+ * Utility to evaluate value of parameters (uses value when given
+ * or invokes a callback to get the value) and calls [action] for
+ * each non-null element in this selection
+ */
+ void _do(SelectionCallback f, Function action) {
+ each((d, i, e) => action(e, f == null ? null : f(scope.datum(e), i, e)));
+ }
+
+ /** Calls a function on each non-null element in the selection */
+ void each(SelectionCallback fn) {
+ if (fn == null) return;
+ groups.forEach((SelectionGroup g) {
+ var index = 0;
+ g.elements.forEach((Element e) {
+ if (e != null) fn(scope.datum(e), index, e);
+ index++;
+ });
+ });
+ }
+
+ void on(String type, [SelectionCallback listener, bool capture]) {
+ Function getEventHandler(i, e) => (Event event) {
+ var previous = scope.event;
+ scope.event = event;
+ try {
+ Function.apply(listener, [scope.datum(e), i, e]);
+ } finally {
+ scope.event = previous;
+ }
+ };
+
+ if (!type.startsWith('.')) {
+ if (listener != null) {
+ // Add a listener to each element.
+ each((d, i, Element e){
+ var handlers = scope._listeners[e];
+ if (handlers == null) scope._listeners[e] = handlers = {};
+ handlers[type] = new Pair(getEventHandler(i, e), capture);
+ e.addEventListener(type, handlers[type].first, capture);
+ });
+ } else {
+ // Remove the listener from each element.
+ each((d, i, Element e) {
+ var handlers = scope._listeners[e];
+ if (handlers != null && handlers[type] != null) {
+ e.removeEventListener(
+ type, handlers[type].first, handlers[type].last);
+ }
+ });
+ }
+ } else {
+ // Remove all listeners on the event type (ignoring the namespace)
+ each((d, i, Element e) {
+ var handlers = scope._listeners[e],
+ keys = handlers.keys,
+ t = type.substring(1);
+ keys.forEach((String s) {
+ if (s.split('.')[0] == t) {
+ e.removeEventListener(s, handlers[s].first, handlers[s].last);
+ }
+ });
+ });
+ }
+ }
+
+ int get length {
+ int retval = 0;
+ each((d, i, e) => retval++);
+ return retval;
+ }
+
+ bool get isEmpty => length == 0;
+
+ /** First non-null element in this selection */
+ Element get first {
+ for (int gi = 0; gi < groups.length; gi++) {
+ SelectionGroup g = groups.elementAt(gi);
+ for (int ei = 0; ei < g.elements.length; ei++) {
+ if (g.elements.elementAt(ei) != null) {
+ return g.elements.elementAt(ei);
+ }
+ }
+ }
+ return null;
+ }
+
+ void attr(String name, val) {
+ assert(name != null && name.isNotEmpty);
+ attrWithCallback(name, toCallback(val));
+ }
+
+ void attrWithCallback(String name, SelectionCallback fn) {
+ assert(fn != null);
+ _do(fn, (e, v) => v == null ?
+ e.attributes.remove(name) : e.attributes[name] = "$v");
+ }
+
+ void classed(String name, [bool val = true]) {
+ assert(name != null && name.isNotEmpty);
+ classedWithCallback(name, toCallback(val));
+ }
+
+ void classedWithCallback(String name, SelectionCallback<bool> fn) {
+ assert(fn != null);
+ _do(fn, (e, v) =>
+ v == false ? e.classes.remove(name) : e.classes.add(name));
+ }
+
+ void style(String property, val, {String priority}) {
+ assert(property != null && property.isNotEmpty);
+ styleWithCallback(property,
+ toCallback(val as String), priority: priority);
+ }
+
+ void styleWithCallback(String property,
+ SelectionCallback<String> fn, {String priority}) {
+ assert(fn != null);
+ _do(fn, (Element e, String v) =>
+ v == null || v.isEmpty ?
+ e.style.removeProperty(property) :
+ e.style.setProperty(property, v, priority));
+ }
+
+ void text(String val) => textWithCallback(toCallback(val));
+
+ void textWithCallback(SelectionCallback<String> fn) {
+ assert(fn != null);
+ _do(fn, (e, v) => e.text = v == null ? '' : v);
+ }
+
+ void html(String val) => htmlWithCallback(toCallback(val));
+
+ void htmlWithCallback(SelectionCallback<String> fn) {
+ assert(fn != null);
+ _do(fn, (e, v) => e.innerHtml = v == null ? '' : v);
+ }
+
+ void remove() => _do(null, (e, _) => e.remove());
+
+ Selection select(String selector) {
+ assert(selector != null && selector.isNotEmpty);
+ return new _SelectionImpl.single(selector: selector, source: this);
+ }
+
+ Selection selectWithCallback(SelectionCallback<Element> fn) {
+ assert(fn != null);
+ return new _SelectionImpl.single(fn: fn, source:this);
+ }
+
+ Selection append(String tag) {
+ assert(tag != null && tag.isNotEmpty);
+ return appendWithCallback(
+ (d, ei, e) => Namespace.createChildElement(tag, e));
+ }
+
+ Selection appendWithCallback(SelectionCallback<Element> fn) {
+ assert(fn != null);
+ return new _SelectionImpl.single(fn: (datum, ei, e) {
+ Element child = fn(datum, ei, e);
+ return child == null ? null : e.append(child);
+ }, source: this);
+ }
+
+ Selection insert(String tag,
+ {String before, SelectionCallback<Element> beforeFn}) {
+ assert(tag != null && tag.isNotEmpty);
+ return insertWithCallback(
+ (d, ei, e) => Namespace.createChildElement(tag, e),
+ before: before, beforeFn: beforeFn);
+ }
+
+ Selection insertWithCallback(SelectionCallback<Element> fn,
+ {String before, SelectionCallback<Element> beforeFn}) {
+ assert(fn != null);
+ beforeFn =
+ before == null ? beforeFn : (d, ei, e) => e.querySelector(before);
+ return new _SelectionImpl.single(
+ fn: (datum, ei, e) {
+ Element child = fn(datum, ei, e);
+ Element before = beforeFn(datum, ei, e);
+ return child == null ? null : e.insertBefore(child, before);
+ },
+ source: this);
+ }
+
+ Selection selectAll(String selector) {
+ assert(selector != null && selector.isNotEmpty);
+ return new _SelectionImpl.all(selector: selector, source: this);
+ }
+
+ Selection selectAllWithCallback(SelectionCallback<Iterable<Element>> fn) {
+ assert(fn != null);
+ return new _SelectionImpl.all(fn: fn, source:this);
+ }
+
+ DataSelection data(Iterable vals, [SelectionKeyFunction keyFn]) {
+ assert(vals != null);
+ return dataWithCallback(toCallback(vals), keyFn);
+ }
+
+ DataSelection dataWithCallback(
+ SelectionCallback<Iterable> fn, [SelectionKeyFunction keyFn]) {
+ assert(fn != null);
+
+ var enterGroups = [],
+ updateGroups = [],
+ exitGroups = [];
+
+ // Create a dummy node to be used with enter() selection.
+ Element dummy(val) {
+ var element = new Element.tag('charted-dummy');
+ scope.associate(element, val);
+ return element;
+ };
+
+ // Joins data to all elements in the group.
+ void join(SelectionGroup g, Iterable vals) {
+ // Nodes exiting, entering and updating in this group.
+ // We maintain the nodes at the same index as they currently
+ // are (for exiting) or where they should be (for entering and updating)
+ var update = new List(vals.length),
+ enter = new List(vals.length),
+ exit = new List(g.elements.length);
+
+ // Use the key function to determine the DOM element to data
+ // associations.
+ if (keyFn != null) {
+ var keysOnDOM = [],
+ elementsByKey = {},
+ valuesByKey = {},
+ ei = 0,
+ vi = 0;
+
+ // Create a key to DOM element map.
+ // Used later to see if an element already exists for a key.
+ g.elements.forEach((e) {
+ var keyValue = keyFn(scope.datum(e));
+ if (elementsByKey.containsKey(keyValue)) {
+ exit[ei] = e;
+ } else {
+ elementsByKey[keyValue] = e;
+ }
+ keysOnDOM.add(keyValue);
+ ei++;
+ });
+
+ // Iterate through the values and find values that don't have
+ // corresponding elements in the DOM, collect the entering elements.
+ vals.forEach((v) {
+ var keyValue = keyFn(v);
+ Element e = elementsByKey[keyValue];
+ if (e != null) {
+ update[vi] = e;
+ scope.associate(e, v);
+ } else if (!valuesByKey.containsKey(keyValue)) {
+ enter[vi] = dummy(v);
+ }
+ valuesByKey[keyValue] = v;
+ elementsByKey.remove(keyValue);
+ vi++;
+ });
+
+ // Iterate through the previously saved keys to
+ // find a list of elements that don't have data anymore.
+ // We don't use elementsByKey.keys() becuase that does not
+ // guarantee the order of returned keys.
+ for (int i = 0; i < g.elements.length; i++) {
+ if (elementsByKey.containsKey(keysOnDOM[i])) {
+ exit[i] = g.elements.elementAt(i);
+ }
+ }
+ } else {
+ // When we don't have the key function, just use list index as the key
+ int updateElementsCount = math.min(g.elements.length, vals.length);
+ int i = 0;
+
+ // Collect a list of elements getting updated in this group
+ for (; i < updateElementsCount; i++) {
+ var e = g.elements.elementAt(i);
+ if (e != null) {
+ scope.associate(e, vals.elementAt(i));
+ update[i] = e;
+ } else {
+ enter[i] = dummy(vals.elementAt(i));
+ }
+ }
+
+ // List of elements newly getting added
+ for (; i < vals.length; i++) {
+ enter[i] = dummy(vals.elementAt(i));
+ }
+
+ // List of elements exiting this group
+ for (; i < g.elements.length; i++) {
+ exit[i] = g.elements.elementAt(i);
+ }
+ }
+
+ // Create the element groups and set parents from the current group.
+ enterGroups.add(new _SelectionGroupImpl(enter, parent: g.parent));
+ updateGroups.add(new _SelectionGroupImpl(update, parent: g.parent));
+ exitGroups.add(new _SelectionGroupImpl(exit, parent: g.parent));
+ };
+
+ var index = 0;
+ groups.forEach((SelectionGroup g) {
+ join(g, fn(scope.datum(g.parent), index++, g.parent));
+ });
+
+ return new _DataSelectionImpl(
+ updateGroups, enterGroups, exitGroups, scope);
+ }
+
+ void datum(Iterable vals) {
+ throw new UnimplementedError();
+ }
+
+ void datumWithCallback(SelectionCallback<Iterable> fn) {
+ throw new UnimplementedError();
+ }
+
+ Transition transition() {
+ return _transition = new Transition(this);
+ }
+}
+
+/* Implementation of [DataSelection] */
+class _DataSelectionImpl extends _SelectionImpl implements DataSelection {
+ EnterSelection enter;
+ ExitSelection exit;
+
+ _DataSelectionImpl(Iterable updated, Iterable entering, Iterable exiting,
+ SelectionScope scope) : super.selectionGroups(updated, scope) {
+ enter = new _EnterSelectionImpl(entering, this);
+ exit = new _ExitSelectionImpl(exiting, this);
+ }
+}
+
+/* Implementation of [EnterSelection] */
+class _EnterSelectionImpl implements EnterSelection {
+ final DataSelection update;
+
+ SelectionScope scope;
+ Iterable<SelectionGroup> groups;
+
+ _EnterSelectionImpl(Iterable this.groups, DataSelection this.update) {
+ scope = update.scope;
+ }
+
+ bool get isEmpty => false;
+
+ Selection insert(String tag,
+ {String before, SelectionCallback<Element> beforeFn}) {
+ assert(tag != null && tag.isNotEmpty);
+ return insertWithCallback(
+ (d, ei, e) => Namespace.createChildElement(tag, e),
+ before: before, beforeFn: beforeFn);
+ }
+
+ Selection insertWithCallback(SelectionCallback<Element> fn,
+ {String before, SelectionCallback<Element> beforeFn}) {
+ assert(fn != null);
+ return selectWithCallback((d, ei, e) {
+ Element child = fn(d, ei, e);
+ e.insertBefore(child, e.querySelector(before));
+ return child;
+ });
+ }
+
+ Selection append(String tag) {
+ assert(tag != null && tag.isNotEmpty);
+ return appendWithCallback(
+ (d, ei, e) => Namespace.createChildElement(tag, e));
+ }
+
+ Selection appendWithCallback(SelectionCallback<Element> fn) {
+ assert(fn != null);
+ return selectWithCallback((datum, ei, e) {
+ Element child = fn(datum, ei, e);
+ e.children.add(child);
+ return child;
+ });
+ }
+
+ Selection select(String selector) {
+ assert(selector == null && selector.isNotEmpty);
+ return selectWithCallback((d, ei, e) => e.querySelector(selector));
+ }
+
+ Selection selectWithCallback(SelectionCallback<Element> fn) {
+ var subgroups = [],
+ gi = 0;
+ groups.forEach((SelectionGroup g) {
+ var u = update.groups.elementAt(gi),
+ subgroup = [],
+ ei = 0;
+ g.elements.forEach((Element e) {
+ if (e != null) {
+ var datum = scope.datum(e),
+ selected = fn(datum, ei, g.parent);
+ scope.associate(selected, datum);
+ u.elements[ei] = selected;
+ subgroup.add(selected);
+ } else {
+ subgroup.add(null);
+ }
+ ei++;
+ });
+ subgroups.add(new _SelectionGroupImpl(subgroup, parent: g.parent));
+ gi++;
+ });
+ return new _SelectionImpl.selectionGroups(subgroups, scope);
+ }
+}
+
+/* Implementation of [ExitSelection] */
+class _ExitSelectionImpl extends _SelectionImpl implements ExitSelection {
+ final DataSelection update;
+ _ExitSelectionImpl(Iterable groups, DataSelection update)
+ : update = update, super.selectionGroups(groups, update.scope);
+}
+
+class _SelectionGroupImpl implements SelectionGroup {
+ Iterable<Element> elements;
+ Element parent;
+ _SelectionGroupImpl(this.elements, {this.parent});
+}
diff --git a/pub/charted/lib/selection/selection_scope.dart b/pub/charted/lib/selection/selection_scope.dart
new file mode 100644
index 0000000..1462848
--- /dev/null
+++ b/pub/charted/lib/selection/selection_scope.dart
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+part of charted.selection;
+
+/** SelectionScope represents a scope for all the data and DOM operations. */
+class SelectionScope {
+ Expando _associations = new Expando();
+ Expando<Map<String, Pair<Function, bool>>> _listeners = new Expando();
+ Element _root;
+
+ /** Creates a new selection scope with document as the root. */
+ SelectionScope() {
+ _root = document.documentElement;
+ }
+
+ /**
+ * Creates a new selection scope with the first element matching
+ * [selector] as the root.
+ */
+ SelectionScope.selector(String selector) {
+ if (selector == null || selector.isEmpty ){
+ throw new ArgumentError('Selector cannot be empty');
+ }
+ _root = document.querySelector(selector);
+ }
+
+ /**
+ * Creates a new selection scope with the passed [element] as [root].
+ * Charted assumes that the element is already part of the DOM.
+ */
+ SelectionScope.element(Element element) {
+ if (element == null) {
+ throw new ArgumentError('Root element for SelectionScope cannot be null');
+ }
+ _root = element;
+ }
+
+ /**
+ * Returns the [root] element for this scope.
+ * All [Selection]s and elements created using the Charted API,
+ * are created as decendants of root.
+ */
+ Element get root => _root;
+
+ /*
+ * Current event for which a callback is being called.
+ */
+ Event event;
+
+ /** Returns the stored for the given [element]. */
+ datum(Element element) => element == null ? null : _associations[element];
+
+ /** Associates data to the given [element]. */
+ associate(Element element, datum) =>
+ datum != null ? _associations[element] = datum : null;
+
+ /**
+ * Creates a new [Selection] containing the first element matching
+ * [selector]. If no element matches, the resulting selection will
+ * have a null element.
+ */
+ Selection select(String selector) =>
+ new _SelectionImpl.single(selector: selector, scope: this);
+
+ /**
+ * Creates a new [Selection] containing all elements matching [selector].
+ * If no element matches, the resulting selection will not have any
+ * elements in it.
+ */
+ Selection selectAll(String selector) =>
+ new _SelectionImpl.all(selector:selector, scope:this);
+
+ /**
+ * Creates a new [Selection] containing [elements]. Assumes that
+ * all the given elements are decendants of [root] in DOM.
+ */
+ Selection selectElements(List<Element> elements) =>
+ new _SelectionImpl.elements(elements, this);
+
+ /**
+ * Appends a new element to [root] and creates a selection containing
+ * the newly added element.
+ */
+ Selection append(String tag) {
+ var element = Namespace.createChildElement(tag, _root);
+ root.children.add(element);
+
+ return selectElements([element]);
+ }
+}
diff --git a/pub/charted/lib/svg/svg.dart b/pub/charted/lib/svg/svg.dart
new file mode 100644
index 0000000..0b48b60
--- /dev/null
+++ b/pub/charted/lib/svg/svg.dart
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+/*
+ * TODO(prsd): Document library
+ */
+library charted.svg;
+
+import "dart:math" as math;
+import "dart:html" show Element;
+import "package:charted/core/core.dart";
+import 'package:charted/interpolators/interpolators.dart';
+import "package:charted/scale/scale.dart";
+import "package:charted/selection/selection.dart";
+
+part 'svg_arc.dart';
+part 'svg_axis.dart';
+part 'svg_line.dart';
+
+/**
+ * Common interface supported by all path generators.
+ */
+abstract class SvgPathGenerator {
+ /**
+ * Generate the path based on the passed data [d], element index [i]
+ * and the element [e]. This method follows the same signature as the
+ * other callbacks used by selection API - [ChartedCallback<String>]
+ */
+ String path(d, int i, Element e);
+}
diff --git a/pub/charted/lib/svg/svg_arc.dart b/pub/charted/lib/svg/svg_arc.dart
new file mode 100644
index 0000000..50c5198
--- /dev/null
+++ b/pub/charted/lib/svg/svg_arc.dart
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.svg;
+
+/**
+ * [SvgArc] provides a data-driven way to create Svg path descriptions
+ * that can be used to draw arcs - like those used in pie-charts.
+ */
+class SvgArc implements SvgPathGenerator {
+ static const num _OFFSET = -HALF_PI;
+ static const num _MAX = TAU - EPSILON;
+
+ /**
+ * [innerRadiusCallback] is called to get inner radius of the arc.
+ * As with other callbacks, [innerRadiusCallback] is passed data, index
+ * and element in the context.
+ *
+ * If not specified, [defaultInnerRadiusCallback] is used.
+ */
+ SelectionCallback<num> innerRadiusCallback = defaultInnerRadiusCallback;
+
+ /**
+ * [outerRadiusCallback] is called to get outer radius of the arc.
+ * As with other callbacks, [outerRadiusCallback] is passed data, index
+ * and element in the context.
+ *
+ * If not specified, [defaultOuterRadiusCallback] is used.
+ */
+ SelectionCallback<num> outerRadiusCallback = defaultOuterRadiusCallback;
+
+ /**
+ * [startAngleCallback] is called to get the start angle of the arc.
+ * As with other callbacks, [startAngleCallback] is passed data, index
+ * and element in the context.
+ *
+ * If not specified, [defaultStartAngleCallback] is used.
+ */
+ SelectionCallback<num> startAngleCallback = defaultStartAngleCallback;
+
+ /**
+ * [endAngleCallback] is called to get the start angle of the arc.
+ * As with other callbacks, [endAngleCallback] is passed data, index
+ * and element in the context.
+ *
+ * If not specified, [defaultEndAngleCallback] is used.
+ */
+ SelectionCallback<num> endAngleCallback = defaultEndAngleCallback;
+
+ /** Sets a constant value as inner radius of the arc. */
+ set innerRadius(num value) => innerRadiusCallback = toCallback(value);
+
+ /** Sets a constant value as outer radius of the arc. */
+ set outerRadius(num value) => outerRadiusCallback = toCallback(value);
+
+ /** Sets a constant value as start angle of the arc. */
+ set startAngle(num value) => startAngleCallback = toCallback(value);
+
+ /** Sets a constant value as end angle of the arc. */
+ set endAngle(num value) => endAngleCallback = toCallback(value);
+
+ String path(d, int i, Element e) {
+ var ir = innerRadiusCallback(d, i, e),
+ or = outerRadiusCallback(d, i, e),
+ start = startAngleCallback(d, i, e) + _OFFSET,
+ end = endAngleCallback(d, i, e) + _OFFSET,
+ sa = math.min(start, end),
+ ea = math.max(start, end),
+ delta = ea - sa;
+
+ if (delta > _MAX) {
+ return ir > 0 ?
+ // Concentric circles with area between them
+ "M0,$or" "A$or,$or 0 1,1 0,-$or" "A$or,$or 0 1,1 0,$or"
+ "M0,$ir" "A$ir,$ir 0 1,0 0,-$ir" "A$ir,$ir 0 1,0 0,$ir" "Z" :
+ // Circle with enclosed area
+ "M0,$or" "A$or,$or 0 1,1 0,-$or" "A$or,$or 0 1,1 0,$or" "Z";
+ }
+
+ var ss = math.sin(sa),
+ se = math.sin(ea),
+ cs = math.cos(sa),
+ ce = math.cos(ea),
+ df = delta < PI ? 0 : 1;
+
+ return ir > 0 ?
+ // Arc capped at ends with lines
+ "M${or * cs},${or * ss}" "A$or,$or 0 $df,1 ${or * ce},${or * se}"
+ "L${ir * ce},${ir * se}" "A$ir,$ir 0 $df,0 ${ir * cs},${ir * ss}"
+ "Z" :
+ // A circular sector
+ "M${or * cs},${or * ss}" "A$or,$or 0 $df,1 ${or * ce},${or * se}"
+ "L0,0" "Z";
+ }
+
+ List centroid(d, int i, Element e) {
+ var r = (innerRadiusCallback(d, i, e) + outerRadiusCallback(d, i, e)) / 2,
+ a = (startAngleCallback(d, i, e) + endAngleCallback(d, i, e)) / 2 -
+ math.PI / 2;
+ return [math.cos(a) * r, math.sin(a) * r];
+ }
+
+ /** Default [innerRadiusCallback] returns data.innerRadius */
+ static num defaultInnerRadiusCallback(d, i, e) => d is !SvgArcData ||
+ d == null || d.innerRadius == null ? 0 : d.innerRadius;
+
+ /** Default [outerRadiusCallback] returns data.outerRadius */
+ static num defaultOuterRadiusCallback(d, i, e) => d is !SvgArcData ||
+ d == null || d.outerRadius == null ? 0 : d.outerRadius;
+
+ /** Default [startAngleCallback] returns data.startAngle */
+ static num defaultStartAngleCallback(d, i, e) => d is !SvgArcData ||
+ d == null || d.startAngle == null ? 0 : d.startAngle;
+
+ /** Default [endAngleCallback] that returns data.endAngle */
+ static num defaultEndAngleCallback(d, i, e) => d is !SvgArcData ||
+ d == null || d.endAngle == null ? 0 : d.endAngle;
+}
+
+/** Value type for SvgArc as used by default property accessors in SvgArc */
+class SvgArcData {
+ var data;
+ num value;
+ num innerRadius;
+ num outerRadius;
+ num startAngle = 0;
+ num endAngle = 2 * PI;
+
+ SvgArcData(this.data, this.value, this.startAngle,
+ this.endAngle, [this.innerRadius=0, this.outerRadius=100]);
+}
+
+
+/**
+ * Returns the interpolator between two [SvgArcData] [a] and [b].
+ *
+ * The interpolator will interpolate the older innerRadius and outerDadius with
+ * newer ones, as well as older startAngle and endAngle with newer ones.
+ *
+ */
+InterpolateFn interpolateSvgArcData(SvgArcData a, SvgArcData b) {
+ var ast = a.startAngle,
+ aen = a.endAngle,
+ ai = a.innerRadius,
+ ao = a.outerRadius,
+ bst = b.startAngle - ast,
+ ben = b.endAngle - aen,
+ bi = b.innerRadius - ai,
+ bo = b.outerRadius - ao;
+
+ return (t) => new SvgArcData(b.data, b.value,
+ (ast + bst * t), (aen + ben * t), (ai + bi * t), (ao + bo * t));
+}
+
diff --git a/pub/charted/lib/svg/svg_axis.dart b/pub/charted/lib/svg/svg_axis.dart
new file mode 100644
index 0000000..d86b01d
--- /dev/null
+++ b/pub/charted/lib/svg/svg_axis.dart
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.svg;
+
+typedef String Formatter(x);
+
+/**
+ * [SvgAxis] helps draw chart axes based on a given scale.
+ */
+class SvgAxis {
+ /** Scale used on this axis */
+ Scale scale = new LinearScale();
+
+ /** Orientation of the axis. Defaults to [ORIENTATION_BOTTOM] */
+ String orientation = ORIENTATION_BOTTOM;
+
+ /** Size of all inner ticks */
+ num innerTickSize = 6;
+
+ /** Size of the outer two ticks */
+ num outerTickSize = 6;
+
+ /** Padding on the ticks */
+ num tickPadding = 3;
+
+ /** Suggested number of ticks to be displayed on the axis */
+ num suggestedTickCount = 10;
+
+ /** List of values to be used on the ticks */
+ List tickValues;
+
+ /** Formatter for the tick labels */
+ Formatter tickFormat;
+
+ /** Previous rotate angle */
+ num _prevRotate = 0;
+
+ /* Store of axis roots mapped to currently used scales */
+ static Expando<Scale> _scales = new Expando<Scale>();
+
+ axis(Selection g) => g.each((d, i, e) => _create(e, g.scope));
+
+ _create(Element e, SelectionScope scope) {
+ var group = scope.selectElements([e]),
+ older = _scales[e],
+ current = _scales[e] = scale.copy();
+
+ if (older == null) older = scale;
+ var tickFormat = this.tickFormat == null ?
+ current.tickFormat(suggestedTickCount) : this.tickFormat,
+ tickValues = this.tickValues == null ?
+ current.ticks(suggestedTickCount) : this.tickValues;
+
+ var ticks = group.selectAll('.tick').data(tickValues, current.apply),
+ tickEnter = ticks.enter.insert('g', before:'.domain')
+ ..classed('tick')
+ ..style('opacity', EPSILON.toString()),
+ tickExit = ticks.exit..remove(),
+ tickUpdate = ticks..style('opacity', '1'),
+ tickTransform;
+
+ var range = current.rangeExtent(),
+ path = group.selectAll('.domain').data([0]);
+ path.enter.append('path');
+ path.attr('class', 'domain');
+
+ num axisLength = range[1] - range[0];
+
+ tickEnter.append('line');
+ tickEnter.append('text');
+
+ var lineEnter = tickEnter.select('line'),
+ lineUpdate = tickUpdate.select('line'),
+ textEnter = tickEnter.select('text'),
+ textUpdate = tickUpdate.select('text'),
+ text = ticks.select('text')
+ ..textWithCallback((d,i,e) => tickFormat(d));
+
+ var ellipsis = group.selectAll('.ellipsis').data(["... ..."]);
+ ellipsis.enter.append('text')
+ ..attr('class', 'ellipsis')
+ ..style('text-anchor', 'middle')
+ ..textWithCallback((d, i, e) => d)
+ ..attr('opacity', 0);
+
+ switch (orientation) {
+ case ORIENTATION_BOTTOM: {
+ tickTransform = _xAxisTransform;
+ ticks.attr('y2', innerTickSize);
+ lineEnter.attr('y2', innerTickSize);
+ textEnter.attr('y', math.max(innerTickSize, 0) + tickPadding);
+ lineUpdate
+ ..attr('x2', 0)
+ ..attr('y2', innerTickSize);
+ textUpdate
+ ..attr('x', 0)
+ ..attr('y', math.max(innerTickSize, 0) + tickPadding);
+ textEnter
+ ..attr('dy', '.71em')
+ ..style('text-anchor', 'middle');
+ ellipsis
+ ..attr('x', axisLength / 2)
+ ..attr('y', '.71em');
+ path.attr('d',
+ 'M${range[0]},${outerTickSize}V0H${range[1]}V${outerTickSize}');
+ }
+ break;
+ case ORIENTATION_TOP: {
+ tickTransform = _xAxisTransform;
+ lineEnter.attr('y2', -innerTickSize);
+ textEnter.attr('y', -(math.max(innerTickSize, 0) + tickPadding));
+ lineUpdate
+ ..attr('x2', 0)
+ ..attr('y2', -innerTickSize);
+ textUpdate
+ ..attr('x', 0)
+ ..attr('y', -(math.max(innerTickSize, 0) + tickPadding));
+ textEnter
+ ..attr('dy', '0em')
+ ..style('text-anchor', 'middle');
+ ellipsis
+ ..attr('x', axisLength / 2)
+ ..attr('y', 0);
+ path.attr('d',
+ 'M${range[0]},${-outerTickSize}V0H${range[1]}V${-outerTickSize}');
+ }
+ break;
+ case ORIENTATION_LEFT: {
+ tickTransform = _yAxisTransform;
+ lineEnter.attr('x2', -innerTickSize);
+ textEnter.attr('x', -(math.max(innerTickSize, 0) + tickPadding));
+ lineUpdate
+ ..attr('x2', -innerTickSize)
+ ..attr('y2', 0);
+ textUpdate
+ ..attr('x', -(math.max(innerTickSize, 0) + tickPadding))
+ ..attr('y', 0);
+ textEnter
+ ..attr('dy', '.32em')
+ ..style('text-anchor', 'end');
+ ellipsis
+ ..attr("transform",
+ "translate(${-tickPadding}, ${axisLength / 2})rotate(90)");
+ path.attr('d',
+ 'M${-outerTickSize},${range[0]}H0V${range[1]}H${-outerTickSize}');
+ }
+ break;
+ case ORIENTATION_RIGHT: {
+ tickTransform = _yAxisTransform;
+ lineEnter.attr('x2', innerTickSize);
+ textEnter.attr('x', math.max(innerTickSize, 0) + tickPadding);
+ lineUpdate
+ ..attr('x2', innerTickSize)
+ ..attr('y2', 0);
+ textUpdate
+ ..attr('x', math.max(innerTickSize, 0) + tickPadding)
+ ..attr('y', 0);
+ textEnter
+ ..attr('dy', '.32em')
+ ..style('text-anchor', 'start');
+ ellipsis
+ ..attr("transform",
+ "translate(${tickPadding}, ${axisLength / 2})rotate(90)");
+ path.attr('d',
+ 'M${outerTickSize},${range[0]}H0V${range[1]}H${outerTickSize}');
+ }
+ break;
+ }
+
+ num currentRotate = 0;
+ num prevRotate = _prevRotate;
+
+ lineEnter..attr('opacity', '0');
+ textEnter..attr('opacity', '0');
+ lineEnter.transition()
+ ..attr('opacity', '1')
+ ..delay(100);
+
+ List textOpacity = new List.filled(text.length, 1);
+
+ if (orientation == ORIENTATION_BOTTOM || orientation == ORIENTATION_TOP) {
+ num maxTextWidth = 0,
+ textHeight = text.first.clientHeight;
+ text.each((d, i, e)
+ => maxTextWidth = math.max(maxTextWidth, e.clientWidth));
+ if (maxTextWidth > axisLength / text.length) currentRotate = 45;
+ if (textHeight * 1.5 > axisLength / text.length) {
+ currentRotate = 90;
+ }
+ _prevRotate = currentRotate;
+ List preTransX = new List(),
+ nowTransX = new List(),
+ preTransY = new List(),
+ nowTransY = new List();
+ num prevArc = prevRotate * math.PI / 180,
+ nowArc = currentRotate * math.PI / 180;
+ text.each((d, i, e) {
+ preTransX.add(prevRotate > 0 ?
+ e.clientWidth * math.cos(prevArc) / 2 : 0);
+ nowTransX.add(currentRotate > 0 ? e.clientWidth * math.cos(nowArc) / 2 : 0);
+ preTransY.add(e.clientWidth * math.sin(prevArc) / 2);
+ nowTransY.add(e.clientWidth * math.sin(nowArc) / 2);
+ });
+ text.transition()
+ ..attrTween('transform', (d, i, e) {
+ return interpolateTransform(
+ "translate(${preTransX[i]},${preTransY[i]})rotate(${prevRotate})",
+ "translate(${nowTransX[i]},${nowTransY[i]})rotate(${currentRotate})");
+ })
+ ..attr('dx', '${.71 * math.sin(nowArc)}em')
+ ..attr('dy', '${.71 * math.cos(nowArc)}em');
+ if (currentRotate == 90) {
+ text.each((d, i, Element e) {
+ if (i > 0 && i < text.length - 1) {
+ textOpacity[i] = 0;
+ }
+ group.selectAll('.ellipsis').transition()
+ ..delay(100)
+ ..attr('opacity', '1');
+ });
+ } else {
+ group.selectAll('.ellipsis').transition()
+ ..delay(100)
+ ..attr('opacity', '0');
+ }
+ } else {
+ num textHeight = text.first.clientHeight;
+ if (textHeight > axisLength / text.length) {
+ text.each((d, i, Element e) {
+ if (i > 0 && i < text.length - 1) {
+ textOpacity[i] = 0;
+ }
+ group.selectAll('.ellipsis').transition()
+ ..delay(100)
+ ..attr('opacity', '1');
+ });
+ } else {
+ group.selectAll('.ellipsis').transition()
+ ..delay(100)
+ ..attr('opacity', '0');
+ }
+ }
+
+ text.transition()
+ ..attrWithCallback('opacity', (d, i, e) => textOpacity[i])
+ ..delay(100);
+
+ // If either the new or old scale is ordinal,
+ // entering ticks are undefined in the old scale,
+ // and so can fade-in in the new scale’s position.
+ // Exiting ticks are likewise undefined in the new scale,
+ // and so can fade-out in the old scale’s position.
+ var transformFn;
+ if (current.rangeBand != 0) {
+ var dx = current.rangeBand / 2;
+ transformFn = (d) => current.apply(d) + dx;
+ } else if (older.rangeBand != 0) {
+ older = current;
+ } else {
+ tickTransform(tickExit, current.apply);
+ }
+
+ tickTransform(tickEnter, transformFn != null ? transformFn : older.apply);
+ tickTransform(tickUpdate, transformFn != null ?
+ transformFn : current.apply);
+ }
+
+ _xAxisTransform(selection, transformFn) {
+ selection.transition()
+ ..attrWithCallback('transform', (d, i, e) =>
+ 'translate(${transformFn(d)},0)'
+ );
+ }
+
+ _yAxisTransform(selection, transformFn) {
+ selection.transition()
+ ..attrWithCallback('transform', (d, i, e) =>
+ 'translate(0,${transformFn(d)})'
+ );
+ }
+}
diff --git a/pub/charted/lib/svg/svg_line.dart b/pub/charted/lib/svg/svg_line.dart
new file mode 100644
index 0000000..d99d6ad
--- /dev/null
+++ b/pub/charted/lib/svg/svg_line.dart
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.svg;
+
+/**
+ * Implementations of line interpolators are given a list of
+ * points and they must return Svg path description to draw
+ * a line connecting these points
+ */
+typedef String LineInterpolator(List<math.Point> points);
+
+/**
+ * [SvgLine] provides a data-driven way to create Svg path descriptions
+ * that can be used to draw lines. It internally has support for a few
+ * interpolations and can easily be extended to add support for more.
+ */
+class SvgLine implements SvgPathGenerator {
+ // TODO(prsd): Implement more interpolators
+
+ /** Linear interpolator */
+ static const String LINEAR = "linear";
+
+ /** Default interpolator */
+ static LineInterpolator DEFAULT_INTERPOLATOR = _linear;
+
+ /** Map of interpolator names to implementations */
+ static Map<String, LineInterpolator> interpolators = {};
+
+ /**
+ * [xAccessor] is used to access/convert datum to x coordinate value.
+ * If not specified, [defaultDataToX] is used.
+ */
+ SelectionValueAccessor<num> xAccessor = defaultDataToX;
+
+ /**
+ * [yAccessor] is used to access/convert datum to y coordinate value.
+ * If not specified, [defaultDataToY] is used.
+ */
+ SelectionValueAccessor<num> yAccessor = defaultDataToY;
+
+ /**
+ * [defined] is used to determine if a value is considered valid.
+ * If this function returns false for any value, that value isn't
+ * included in the line and the line gets split.
+ */
+ SelectionCallback<bool> defined = (d, i, e) => true;
+
+ LineInterpolator _interpolate = DEFAULT_INTERPOLATOR;
+
+ /**
+ * Set line interpolation to one of the known/registered
+ * interpolation types. Defaults to [SvgLine.LINEAR]
+ */
+ set interpolation(String value) {
+ if (!interpolators.containsKey(value)) {
+ throw new ArgumentError('Unregistered line interpolator.');
+ }
+ _interpolate = DEFAULT_INTERPOLATOR;
+ }
+
+ /** Set a constant value as the x-value on all points in the line */
+ set x(num value) => xAccessor = toValueAccessor(value);
+
+ /** Set a constant value as the x-value on all points in the line */
+ set y(num value) => yAccessor = toValueAccessor(value);
+
+ /**
+ * Get Svg path description for the given [data], element index [index]
+ * and element [e] to which the data is associated.
+ */
+ String path(data, int index, Element e) {
+ var segments = [],
+ points = [];
+ assert(data is List);
+ data.asMap().forEach((int i, d) {
+ if (defined(d, i, e)) {
+ points.add(new math.Point(xAccessor(d, i), yAccessor(d, i)));
+ } else if (points.isNotEmpty) {
+ segments.add('M${_interpolate(points)}');
+ points = [];
+ }
+ });
+
+ if (points.isNotEmpty) segments.add('M${_interpolate(points)}');
+
+ return segments.join();
+ }
+
+ /**
+ * Default implementation of a callback to extract X coordinate
+ * value for a given datum. Assumes that datam is a non-empty
+ * array.
+ */
+ static num defaultDataToX(d, i) => d[0];
+
+ /**
+ * Default implementation of a callback to extract Y coordinate
+ * value for a given datum. Assumes that datam is an array of
+ * atleast two elements.
+ */
+ static num defaultDataToY(d, i) => d[1];
+
+ /* Implementation of the linear interpolator */
+ static String _linear(List points) =>
+ points.map((pt) => '${pt.x},${pt.y}').join('L');
+}
diff --git a/pub/charted/lib/time/time.dart b/pub/charted/lib/time/time.dart
new file mode 100644
index 0000000..77b919f
--- /dev/null
+++ b/pub/charted/lib/time/time.dart
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+/*
+ * TODO(songrenchu): Document library
+ */
+library charted.time;
+
+part 'time_interval.dart';
+
+class Time {
+ static TimeInterval second = new TimeInterval(
+ (DateTime date) => new DateTime.fromMillisecondsSinceEpoch(
+ (date.millisecondsSinceEpoch ~/ 1000) * 1000),
+ (DateTime date, num offset) {
+ date = new DateTime.fromMillisecondsSinceEpoch(
+ date.millisecondsSinceEpoch + offset.toInt() * 1000);
+ return date;
+ },
+ (DateTime date) => date.second
+ );
+
+ static TimeInterval minute = new TimeInterval(
+ (DateTime date) => new DateTime.fromMillisecondsSinceEpoch(
+ (date.millisecondsSinceEpoch ~/ 60000) * 60000),
+ (DateTime date, num offset) {
+ date = new DateTime.fromMillisecondsSinceEpoch(
+ date.millisecondsSinceEpoch + offset.toInt() * 60000);
+ return date;
+ },
+ (DateTime date) => date.minute
+ );
+
+ static TimeInterval hour = new TimeInterval(
+ (DateTime date) => new DateTime.fromMillisecondsSinceEpoch(
+ (date.millisecondsSinceEpoch ~/ 3600000) * 3600000),
+ (DateTime date, num offset) {
+ date = new DateTime.fromMillisecondsSinceEpoch(
+ date.millisecondsSinceEpoch + offset.toInt() * 3600000);
+ return date;
+ },
+ (DateTime date) => date.hour
+ );
+
+ static TimeInterval day = new TimeInterval(
+ (DateTime date) => new DateTime(date.year, date.month, date.day),
+ (DateTime date, num offset) =>
+ new DateTime(date.year, date.month, date.day + offset.toInt(),
+ date.hour, date.minute, date.second, date.millisecond),
+ (DateTime date) => date.day - 1
+ );
+
+ // TODO(songrenchu): Implement seven days of a week, now only Sunday as
+ // the first day is supported.
+ static TimeInterval week = new TimeInterval(
+ (DateTime date) =>
+ new DateTime(date.year, date.month, date.day - date.day % 7),
+ (DateTime date, num offset) =>
+ new DateTime(date.year, date.month, date.day + offset.toInt()* 7,
+ date.hour, date.minute, date.second, date.millisecond ),
+ (DateTime date) {
+ var day = year.floor(date).day;
+ return (dayOfYear(date) + day % 7) ~/ 7;
+ }
+ );
+
+ static TimeInterval month = new TimeInterval(
+ (DateTime date) => new DateTime(date.year, date.month, 1),
+ (DateTime date, num offset) =>
+ new DateTime(date.year, date.month + offset.toInt(), date.day,
+ date.hour, date.minute, date.second, date.millisecond),
+ (DateTime date) => date.month - 1
+ );
+
+ static TimeInterval year = new TimeInterval(
+ (DateTime date) => new DateTime(date.year),
+ (DateTime date, num offset) =>
+ new DateTime(date.year + offset.toInt(), date.month, date.day,
+ date.hour, date.minute, date.second, date.millisecond),
+ (DateTime date) => date.year
+ );
+
+ static Function seconds = second.range,
+ minutes = minute.range,
+ hours = hour.range,
+ days = day.range,
+ weeks = week.range,
+ months = month.range,
+ years = year.range;
+
+ static Function secondsUTC = second.rangeUTC,
+ minutesUTC = minute.rangeUTC,
+ hoursUTC = hour.rangeUTC,
+ daysUTC = day.rangeUTC,
+ weeksUTC = week.rangeUTC,
+ monthsUTC = month.rangeUTC,
+ yearsUTC = year.rangeUTC;
+
+ //TODO: better implementation
+ static int dayOfYear(DateTime date) {
+ var floorYear = year.floor(date);
+ return ((date.millisecondsSinceEpoch - floorYear.millisecondsSinceEpoch -
+ (date.timeZoneOffset - floorYear.timeZoneOffset).inMilliseconds * 60000)
+ ~/ 86400000);
+ }
+ //TODO: weekOfYear
+}
\ No newline at end of file
diff --git a/pub/charted/lib/time/time_interval.dart b/pub/charted/lib/time/time_interval.dart
new file mode 100644
index 0000000..b0844a7
--- /dev/null
+++ b/pub/charted/lib/time/time_interval.dart
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.time;
+
+abstract class Interval {
+ DateTime floor(var date);
+ DateTime ceil(var date);
+ List range(var t0, var t1, int dt);
+}
+
+class TimeInterval extends Interval{
+ Function _local;
+ Function _step;
+ Function _number;
+
+ TimeInterval(this._local, this._step, this._number);
+
+ DateTime floor(var date) {
+ if (date is int) date = new DateTime.fromMillisecondsSinceEpoch(date);
+ return _local(date);
+ }
+
+ DateTime round(var date) {
+ if (date is int) date = new DateTime.fromMillisecondsSinceEpoch(date);
+ DateTime d0 = _local(date),
+ d1 = offset(d0, 1);
+ return date.millisecondsSinceEpoch - d0.millisecondsSinceEpoch <
+ d1.millisecondsSinceEpoch - date.millisecondsSinceEpoch ? d0 : d1;
+ }
+
+ DateTime ceil(var date) {
+ if (date is int) date = new DateTime.fromMillisecondsSinceEpoch(date);
+ DateTime d0 = _local(date),
+ d1 = offset(d0, 1);
+ return date.millisecondsSinceEpoch - d0.millisecondsSinceEpoch > 0 ?
+ d1 : d0;
+ }
+
+ offset(date, num k) {
+ date = _step(date, k);
+ return date;
+ }
+
+ List range(var t0, var t1, int dt) {
+ var time = ceil(t0),
+ times = [],
+ t1SinceEpoch = t1 is DateTime ? t1.millisecondsSinceEpoch : t1,
+ timeSinceEpoch = time.millisecondsSinceEpoch;
+ if (dt > 1) {
+ while (timeSinceEpoch < t1SinceEpoch) {
+ if ((_number(time) % dt) == 0) times.add(
+ new DateTime.fromMillisecondsSinceEpoch(timeSinceEpoch));
+ time = _step(time, 1);
+ timeSinceEpoch = time.millisecondsSinceEpoch;
+ }
+ } else {
+ while (timeSinceEpoch < t1SinceEpoch) {
+ times.add(new DateTime.fromMillisecondsSinceEpoch(timeSinceEpoch));
+ time = _step(time, 1);
+ timeSinceEpoch = time.millisecondsSinceEpoch;
+ }
+ }
+ return times;
+ }
+
+ // TODO(songrenchu): Implement UTC range
+ List rangeUTC(DateTime t0, DateTime t1, int dt) {
+ throw new UnimplementedError();
+ }
+}
+
+// TODO(songrenchu): Implement UTC time interval
+class TimeIntervalUTC extends TimeInterval {
+ TimeIntervalUTC(local, step, number): super(local, step, number) {
+ throw new UnimplementedError();
+ }
+}
\ No newline at end of file
diff --git a/pub/charted/lib/transition/transition.dart b/pub/charted/lib/transition/transition.dart
new file mode 100644
index 0000000..238885d
--- /dev/null
+++ b/pub/charted/lib/transition/transition.dart
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+library charted.transition;
+
+import "dart:html" show Element,document;
+import "package:charted/core/core.dart";
+import "package:charted/event/event.dart";
+import "package:charted/selection/selection.dart";
+import "package:charted/interpolators/interpolators.dart";
+
+part "transition_impl.dart";
+
+typedef InterpolateFn AttrTweenCallback(datum, int ei, String attr);
+typedef InterpolateFn StyleTweenCallback(datum, int ei, String style);
+
+/**
+ * Transitions are created using the transition operator on a selection.
+ * Transitions start automatically upon creation after a delay which defaults
+ * to zero; however, note that a zero-delay transition actually starts after a
+ * minimal (~17ms) delay, pending the first timer callback.
+ * Transitions have a default duration of 250ms.
+ */
+abstract class Transition {
+
+ /** A settable default easing type */
+ static EasingFn defaultEasingType = easeCubic();
+
+ /** A settable default easing mode */
+ static EasingMode defaultEasingMode = reflectEasingFn;
+
+ /** A settable default transition duration */
+ static int defaultDuration = 250;
+
+ /** Sets the ease function of the transition, default is cubic-in-out. */
+ InterpolateFn ease;
+
+ /**
+ * Specifies the transition delay in milliseconds. All elements are given the
+ * same delay. The default delay is 0.
+ */
+ void delay(int millisecond);
+
+ /**
+ * Sets the delay with a ChartedCallback function which would be evaluated for
+ * each selected element (in order), being passed the current datum d, the
+ * current index i, and the current DOM element. The function's return value
+ * is then used to set each element's delay.
+ */
+ void delayWithCallback(SelectionCallback fn);
+
+ /**
+ * Specifies per-element duration in milliseconds. All elements are given the
+ * same duration in millisecond. The default duration is 250ms.
+ */
+ void duration(int millisecond);
+
+ /**
+ * Sets the duration with a ChartedCallback which would be evaluated for each
+ * selected element (in order), being passed the current datum d, the current
+ * index i, and the current DOM element. The function's return value is then
+ * used to set each element's duration.
+ */
+ void durationWithCallback(SelectionCallback fn);
+
+ /**
+ * Sets the attribute [name] on all elements when [val] is not null.
+ * Removes the attribute when [val] is null.
+ */
+ void attr(String name, val);
+
+ /**
+ * Same as [attr], but calls [fn] for each non-null element in
+ * the selection (with data associated to the element, index of the
+ * element in it's group and the element itself) to get the value
+ * of the attribute.
+ */
+ void attrWithCallback(String name, SelectionCallback fn);
+
+ /**
+ * Transitions the value of the attribute with the specified name according to
+ * the specified tween function. The starting and ending value of the
+ * transition are determined by tween; the tween function is invoked when the
+ * transition starts on each element, being passed the current datum d, the
+ * current index i and the current attribute value a. The return value of
+ * tween must be an interpolator: a function that maps a parametric value t in
+ * the domain [0,1] to a color, number or arbitrary value.
+ */
+ void attrTween(String name, AttrTweenCallback tween);
+
+ /**
+ * Transitions the value of the CSS style property with the specified name to
+ * the specified value. An optional priority may also be specified, either as
+ * null or the string "important" (without the exclamation point). The
+ * starting value of the transition is the current computed style property
+ * value, and the ending value is the specified value. All elements are
+ * transitioned to the same style property value.
+ */
+ void style(String property, String val, [String priority]);
+
+ /**
+ * Transitions the style with a CartedCallback which would be evaluated for
+ * each selected element (in order), being passed the current datum d and the
+ * current index i, and the current DOM element.
+ * The function's return value is then used to transition each element's
+ * style property.
+ */
+ void styleWithCallback(String property,
+ SelectionCallback<String> fn, [String priority]);
+
+ /**
+ * Transitions the value of the CSS style property with the specified name
+ * according to the specified tween function. An optional priority may also
+ * be specified, either as null or the string "important" (without the
+ * exclamation point). The starting and ending value of the transition are
+ * determined by tween; the tween function is invoked when the transition
+ * starts on each element, being passed the current datum d, the current index
+ * i and the current attribute value a. The return value of tween must be an
+ * interpolator: a function that maps a parametric value t in the domain [0,1]
+ * to a color, number or arbitrary value.
+ */
+ void styleTween(String property, StyleTweenCallback tween, [String priority]);
+
+ /** Interrupts the transition. */
+ void interrupt();
+
+ /**
+ * For each element in the current transition, selects the first descendant
+ * element that matches the specified selector string. If no element matches
+ * the specified selector for the current element, the element at the current
+ * index will be null in the returned selection; operators (with the exception
+ * of data) automatically skip null elements, thereby preserving the index of
+ * the existing selection. If the current element has associated data, this
+ * data is inherited by the returned subselection, and automatically bound to
+ * the newly selected elements. If multiple elements match the selector, only
+ * the first matching element in document traversal order will be selected.
+ */
+ Transition select(String selector);
+
+ /**
+ * For each element in the current transition, selects descendant elements
+ * that match the specified selector string. The returned selection is grouped
+ * by the ancestor node in the current selection. If no element matches the
+ * specified selector for the current element, the group at the current index
+ * will be empty in the returned selection. The subselection does not inherit
+ * data from the current selection; however, if data was previously bound to
+ * the selected elements, that data will be available to operators.
+ */
+ Transition selectAll(String selector);
+
+ /**
+ * Creates a new transition on the same selected elements that starts with
+ * this transition ends. The new transition inherits this transition’s
+ * duration and easing. This can be used to define chained transitions without
+ * needing to listen for "end" events. Only works when parent delay and
+ * duration are constant.
+ */
+ Transition transition();
+
+ /** Factory method to create an instance of the default implementation */
+ factory Transition(Selection selection) => new _TransitionImpl(selection);
+}
\ No newline at end of file
diff --git a/pub/charted/lib/transition/transition_impl.dart b/pub/charted/lib/transition/transition_impl.dart
new file mode 100644
index 0000000..db05b2e
--- /dev/null
+++ b/pub/charted/lib/transition/transition_impl.dart
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+part of charted.transition;
+
+class _TransitionImpl implements Transition {
+
+ SelectionCallback _delay = (d, i, c) => 0;
+ SelectionCallback _duration = (d, i, c) => Transition.defaultDuration;
+ Selection _selection;
+ Map _attrs = {};
+ Map _styles = {};
+ Map _attrTweens = {};
+ Map _styleTweens = {};
+ Map<ChartTimer, Element> _timerMap = {};
+ Map<Element, List<Map>> _attrMap = {};
+ Map<Element, int> _durationMap = {};
+ bool _interrupted = false;
+ var _timerDelay = 0;
+
+ _TransitionImpl(this._selection, [num delay = 0]) {
+ _transitionNode(delay);
+ _timerDelay = delay;
+ }
+
+ InterpolateFn ease = clampEasingFn(Transition.defaultEasingMode(
+ Transition.defaultEasingType));
+
+ void delay([int millisecond = 0]) {
+ delayWithCallback(toCallback(millisecond));
+ }
+
+ void delayWithCallback(SelectionCallback fn) {
+ _delay = fn;
+ }
+
+ void duration(int millisecond) {
+ durationWithCallback(toCallback(millisecond));
+ }
+
+ void durationWithCallback(SelectionCallback fn) {
+ _duration = fn;
+ }
+
+ void attr(String name, val) {
+ attrWithCallback(name, toCallback(val));
+ }
+
+ void attrWithCallback(String name, SelectionCallback fn) {
+ _attrs[name] = fn;
+ }
+
+ void attrTween(String name, AttrTweenCallback tween) {
+ _attrTweens[name] = tween;
+ }
+
+ void style(String property, String val, [String priority = '']) {
+ styleWithCallback(property, toCallback(val), priority);
+ }
+
+ void styleWithCallback(String property, SelectionCallback<String> fn,
+ [String priority = '']) {
+ _styles[property] = {'callback': fn, 'priority': priority};
+ }
+
+ void styleTween(String property, StyleTweenCallback tween,
+ [String priority]) {
+ _styleTweens[property] = {'callback': tween, 'priority': priority};
+ }
+
+ // Starts a timer that registers all attributes, durations, and delays for the
+ // transition of the current selection.
+ _transitionNode(num delay) {
+ new ChartTimer((elapsed) {
+ _selection.each((d, i, c) {
+ var tweenList = [];
+ _attrs.forEach((key, value) {
+ tweenList.add(_getAttrInterpolator(c, key, value(d, i, c)));
+ });
+ _attrTweens.forEach((key, value) {
+ tweenList.add((t) => c.setAttribute(key,
+ value(d, i, c.getAttribute(key))(t)));
+ });
+ _styles.forEach((key, value) {
+ tweenList.add(_getStyleInterpolator(c, key,
+ value['callback'](d, i, c), value['priority']));
+ });
+ _styleTweens.forEach((key, value) {
+ tweenList.add((t) => c.style.setProperty(key,
+ value['callback'](d, i,
+ c.style.getPropertyValue(key))(t).toString(), value['priority']));
+ });
+
+ _attrMap[c] = tweenList;
+ _durationMap[c] = _duration(d, i, c);
+ _timerMap[new ChartTimer(_tick, _delay(d, i, c))] = c;
+ });
+ return true;
+ }, delay);
+ }
+
+ // Returns the correct interpolator function for the old and new attribute.
+ _getAttrInterpolator(Element element, String attrName, newValue) {
+ var attr = element.attributes[attrName];
+ var interpolator = interpolateString(attr, newValue.toString());
+ return (t) => element.setAttribute(attrName, interpolator(t).toString());
+ }
+
+ // Returns the correct interpolator function for the old and new style.
+ _getStyleInterpolator(Element element, String styleName, newValue, priority) {
+ var style = element.style.getPropertyValue(styleName);
+
+ var interpolator = interpolateString(style, newValue.toString());
+
+ return (t) => element.style.setProperty(styleName,
+ interpolator(t).toString(), priority);
+ }
+
+ // Ticks of the transition, this is the callback registered to the
+ // ChartedTimer, called on each animation frame until the transition duration
+ // has been reached.
+ bool _tick(elapsed) {
+ if (_interrupted) {
+ return true;
+ }
+ var activeNode = _timerMap[ChartTimer.activeTimer];
+ var t = elapsed / _durationMap[activeNode];
+ for (InterpolateFn tween in _attrMap[activeNode]) {
+ tween(ease(t));
+ }
+ return (t >= 1) ? true : false;
+ }
+
+ // Interrupts the transition.
+ void interrupt() {
+ _interrupted = true;
+ }
+
+ Transition select(String selector) {
+ var t = new Transition(_selection.select(selector));
+ t.ease = ease;
+ t.delayWithCallback(_delay);
+ t.durationWithCallback(_duration);
+ return t;
+ }
+
+ Transition selectAll(String selector) {
+ var t = new Transition(_selection.selectAll(selector));
+ t.ease = ease;
+ t.delayWithCallback(_delay);
+ t.durationWithCallback(_duration);
+ return t;
+ }
+
+ Transition transition() {
+ var e = _selection.first;
+ var delay = _delay(_selection.scope.datum(e), 0, e) +
+ _duration(_selection.scope.datum(e), 0, e) + _timerDelay;
+ var t = new _TransitionImpl(_selection, delay);
+ t.ease = ease;
+ t.durationWithCallback(_duration);
+ return t;
+ }
+}
diff --git a/pub/charted/pubspec.yaml b/pub/charted/pubspec.yaml
new file mode 100644
index 0000000..09b7d53
--- /dev/null
+++ b/pub/charted/pubspec.yaml
@@ -0,0 +1,17 @@
+name: charted
+version: 0.0.10
+authors:
+- Prasad Sunkari <prsd@google.com>
+- Michael Cheng <midoringo@google.com>
+- Yan Qiao <yqiao@google.com>
+description: Visualization toolkit for Dart. Provides D3 (http://d3js.org) like Selection API, utilities to achieve data driven DOM and provides an easy-to-use Charting library based on the Selection API.
+homepage: https://github.com/google/charted
+dependencies:
+ browser: any
+ csslib: any
+ intl: any
+ logging: any
+ observe: any
+dev_dependencies:
+ hop: '>=0.27.0'
+ unittest: any
diff --git a/pub/charted/tool/hop_runner.dart b/pub/charted/tool/hop_runner.dart
new file mode 100644
index 0000000..d3b3e41
--- /dev/null
+++ b/pub/charted/tool/hop_runner.dart
@@ -0,0 +1,46 @@
+library charted.tool.hop_runner;
+
+import 'package:hop/hop.dart';
+import 'dart:io';
+
+main(List<String> args) {
+ addTask('unit-test', createUnitTestsTask());
+ runHop(args);
+}
+
+Task createUnitTestsTask() =>
+ new Task((TaskContext context) {
+ context.info("Running tests...");
+ return Process.run('./content_shell',
+ ['--dump-render-tree','test/test_in_browser.html']).
+ then((ProcessResult process) =>
+ checkTestsOutput(context, process));
+ });
+
+/**
+ * Reads the output from content_shell and checks for number of tests
+ * passed/failed/erred. This method requires that the tests be done
+ * in simple html unit test configuration - useHtmlConfiguration().
+ */
+void checkTestsOutput(TaskContext context, ProcessResult process) {
+ var output = (process.stdout as String),
+ passRegEx = new RegExp(r"^[0-9]+\s+PASS\s"),
+ failRegEx = new RegExp(r"^[0-9]+\s+FAIL\s"),
+ errorRegEx = new RegExp(r"^[0-9]+\s+ERROR\s"),
+ passCount = 0,
+ failCount = 0,
+ errorCount = 0;
+
+ context.info(output);
+
+ for (var line in output.split('\n')) {
+ if (line.contains(passRegEx)) passCount++;
+ if (line.contains(failRegEx)) failCount++;
+ if (line.contains(errorRegEx)) errorCount++;
+ }
+
+ if (failCount + errorCount > 0) {
+ context.fail('FAIL: $failCount\nERROR: $errorCount');
+ }
+}
+
diff --git a/pub/charted/web/charts_demo.dart b/pub/charted/web/charts_demo.dart
new file mode 100644
index 0000000..278551c
--- /dev/null
+++ b/pub/charted/web/charts_demo.dart
@@ -0,0 +1,36 @@
+
+library charted.demo;
+
+import 'dart:html';
+import 'package:charted/charted.dart';
+
+part 'src/dataset_small.dart';
+part 'src/dataset_large.dart';
+
+class ChartDemo {
+ final String title;
+ final int dimensionAxesCount;
+ final ChartConfig config;
+ final ChartData data;
+ final Element host;
+
+ ChartArea area;
+
+ ChartDemo(this.title, this.host, this.config, this.data,
+ { this.dimensionAxesCount: 1 }) {
+ var chartAreaHost = new DivElement()..classes.add('chart-host'),
+ chartLegendHost = new DivElement()..classes.add('legend-host'),
+ wrapper = new DivElement()
+ ..children.addAll([chartAreaHost, chartLegendHost])
+ ..classes.add('chart-wrapper');
+
+ host.children.addAll(
+ [ new Element.html('<div><h2>$title</h2></div>'), wrapper ]);
+
+ config.legend = new ChartLegend(chartLegendHost);
+ area = new ChartArea(chartAreaHost, data, config,
+ autoUpdate: false, dimensionAxesCount: dimensionAxesCount);
+ }
+
+ void draw() => area.draw();
+}
diff --git a/pub/charted/web/custom_axis_demo.dart b/pub/charted/web/custom_axis_demo.dart
new file mode 100644
index 0000000..a7bc5bf
--- /dev/null
+++ b/pub/charted/web/custom_axis_demo.dart
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+library charted.demo.custom_axis;
+
+import 'dart:html';
+import 'package:charted/charts/charts.dart';
+import 'package:charted/scale/scale.dart';
+
+List COLUMNS = [
+ new ChartColumnSpec(label:'Month', type:ChartColumnSpec.TYPE_STRING),
+ new ChartColumnSpec(label:'Precipitation'),
+ new ChartColumnSpec(label:'High Temperature'),
+ new ChartColumnSpec(label:'Low Temperature'),
+ new ChartColumnSpec(label:'Random'),
+ new ChartColumnSpec(label:'Extra1'),
+ new ChartColumnSpec(label:'Extra2'),
+ new ChartColumnSpec(label:'Extra3'),
+ ];
+
+List DATA = [
+ ['January', 4.50, 27, 46, 1, 20, 23, 1],
+ ['February', 4.61, 60, 28, 10, 15, 45, 23],
+ ['March', 3.26, 32, 49, 100, 4, 34, 1],
+ ['April', 1.46, 63, 49, 30, 34, 89, 3]
+ ];
+
+main() {
+ // Default Chart
+ var series1 = new ChartSeries("one", [1, 3, 2, 6],
+ new StackedBarChartRenderer()),
+ data = new ChartData(COLUMNS, DATA),
+ config = new ChartConfig([series1], [0]),
+ area = new ChartArea(querySelector('.default'),
+ data, config, autoUpdate:false, dimensionAxesCount:1);
+ area.draw();
+
+
+
+ // Chart with custom measure axis with specific domain on the scale.
+ var series2 = new ChartSeries("one", [1, 3, 2, 6],
+ new StackedBarChartRenderer(),
+ // measureAxisId matches the id later used in registerMeasureAxis().
+ measureAxisIds: ['fixed_domain']),
+ data2 = new ChartData(COLUMNS, DATA),
+ config2 = new ChartConfig([series2], [0]);
+
+ // Add custom scale and axis config.
+ var scale = new LinearScale();
+ scale.domain = [0, 1000];
+ var axisConfig = new ChartAxisConfig();
+ axisConfig.title = 'Axis title';
+ axisConfig.scale = scale;
+
+ config2.registerMeasureAxis('fixed_domain', axisConfig);
+ var customAxisChart = new ChartArea(querySelector('.custom-domain'),
+ data2, config2, autoUpdate:false, dimensionAxesCount:1);
+ customAxisChart.draw();
+
+
+
+ // Chart with custom measure axis with specific tick values.
+ var series3 = new ChartSeries("one", [1, 3, 2, 6],
+ new StackedBarChartRenderer(),
+ // measureAxisId matches the id later used in registerMeasureAxis().
+ measureAxisIds: ['fixed_ticks']),
+ data3 = new ChartData(COLUMNS, DATA),
+ config3 = new ChartConfig([series3], [0]);
+
+ // Add custom scale and axis config.
+ var scale2 = new LinearScale();
+ scale2.domain = [0, 300];
+ var axisConfig2 = new ChartAxisConfig();
+ axisConfig2.title = 'Axis title';
+ axisConfig2.scale = scale2;
+ axisConfig2.tickValues = [0, 25, 50, 230, 250];
+
+ config3.registerMeasureAxis('fixed_ticks', axisConfig2);
+ var fixedTickValueChart = new ChartArea(querySelector('.custom-ticks'),
+ data3, config3, autoUpdate:false, dimensionAxesCount:1);
+ fixedTickValueChart.draw();
+}
diff --git a/pub/charted/web/interactive_demo.dart b/pub/charted/web/interactive_demo.dart
new file mode 100644
index 0000000..3f38111
--- /dev/null
+++ b/pub/charted/web/interactive_demo.dart
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+library charted.demo.interactive;
+
+import 'dart:html';
+import 'package:observe/observe.dart';
+import 'package:charted/charts/charts.dart';
+
+import 'charts_demo.dart';
+
+const List DIMENSION_COLUMNS = const[0, 4];
+
+int customSeriesCounter = 0;
+
+List DEMOS = [
+ {
+ 'name': 'One',
+ 'title': 'One — A chart with one dimension axis',
+ 'sub-title': 'Compatible with bar, line and stacked-bar renderers',
+ 'series': [
+ {
+ 'name': 'Series-01',
+ 'renderer': 'bar-chart',
+ 'columns': [ 1, 2, 3 ]
+ }
+ ]
+ }
+];
+
+Map RENDERERS = {
+ 'bar-chart': 'Bar chart',
+ 'line-chart': 'Line chart',
+ 'stacked-bar-chart': 'Stacked bar chart',
+ 'waterfall-chart': 'Waterfall chart',
+};
+
+ChartRenderer getRendererForType(String name) {
+ if (name == 'bar-chart') return new BarChartRenderer();
+ if (name == 'line-chart') return new LineChartRenderer();
+ if (name == 'stacked-bar-chart') return new StackedBarChartRenderer();
+ if (name == 'waterfall-chart') return new WaterfallChartRenderer();
+ return new BarChartRenderer();
+}
+
+String getTypeForRenderer(ChartRenderer renderer) {
+ if (renderer is BarChartRenderer) return 'bar-chart';
+ if (renderer is LineChartRenderer) return 'line-chart';
+ if (renderer is StackedBarChartRenderer) return 'stacked-bar-chart';
+ if (renderer is WaterfallChartRenderer) return 'waterfall-chart';
+ return 'bar-chart';
+}
+
+main() {
+ ChartSeries activeSeries,
+ defaultSeries = new ChartSeries("Default series",
+ new ObservableList.from([ 2, 3 ]), new BarChartRenderer());
+
+ ObservableList rows = new ObservableList.from(SMALL_DATA.sublist(0, 3)),
+ columns = new ObservableList.from(SMALL_DATA_COLUMNS),
+ seriesList = new ObservableList.from([ defaultSeries ]);
+
+ ChartData data = new ChartData(columns, rows);
+ ChartConfig config = new ChartConfig(seriesList, DIMENSION_COLUMNS);
+
+ ChartArea area = new ChartArea(querySelector('.chart-host'), data, config,
+ autoUpdate: true, dimensionAxesCount: 1);
+
+ area.addChartBehavior(new ChartTooltip());
+ config.legend = new ChartLegend(querySelector('.legend-host'));
+
+ area.draw();
+
+ /*
+ * Create and hook up the control panel
+ */
+
+ ButtonElement addRowButton = querySelector('#add-row'),
+ removeRowButton = querySelector('#remove-row'),
+ addSeriesButton = querySelector('#add-series'),
+ removeSeriesButton = querySelector('#remove-series');
+
+ SelectElement seriesSelect = querySelector('#select-series'),
+ rendererSelect = querySelector('#select-renderer');
+
+ Element columnButtons = querySelector('#column-buttons');
+
+
+ /*
+ * Updating rows
+ */
+
+ updateRowButtonStates() {
+ addRowButton.disabled = rows.length >= SMALL_DATA.length;
+ removeRowButton.disabled = rows.length <= 1;
+ }
+
+ addRowButton.onClick.listen((_) {
+ if (rows.length < SMALL_DATA.length) {
+ rows.add(SMALL_DATA.elementAt(rows.length));
+ }
+ updateRowButtonStates();
+ });
+
+ removeRowButton.onClick.listen((_) {
+ if (rows.length > 0) rows.removeLast();
+ updateRowButtonStates();
+ });
+
+
+ /*
+ * Series selection
+ */
+
+ selectSeries(ChartSeries series) {
+ activeSeries = series;
+
+ List<Element> options = rendererSelect.children.toList();
+ String rendererType = getTypeForRenderer(series.renderer);
+ for (int i = 0; i < options.length; i++) {
+ if ((options[i] as OptionElement).value == rendererType)
+ rendererSelect.selectedIndex = i;
+ }
+
+ List<InputElement> buttons = querySelectorAll('.column-button');
+ Iterable measures = series.measures;
+ for (int i = 0; i < buttons.length; i++) {
+ buttons[i].checked = measures.contains(i + 1);
+ }
+ }
+
+ updateSeriesSelector([ChartSeries selected]) {
+ seriesSelect.children.clear();
+ seriesList.forEach((ChartSeries item) {
+ var option = new OptionElement()
+ ..value = item.name
+ ..text = item.name;
+ seriesSelect.append(option);
+ });
+ selectSeries(selected == null ? seriesList.first : selected);
+ }
+
+ seriesSelect.onChange.listen(
+ (_) => selectSeries(seriesList.firstWhere(
+ (ChartSeries x) => x.name == seriesSelect.value)));
+
+
+ /*
+ * Renderer selection for active series
+ */
+
+ updateRendererSelector() {
+ rendererSelect.children.clear();
+ RENDERERS.forEach((name, label) {
+ var option = new OptionElement()
+ ..value = name
+ ..text = label;
+ rendererSelect.append(option);
+ });
+ }
+
+ rendererSelect.onChange.listen((_) {
+ if (rendererSelect.value == "waterfall-chart" &&
+ area.data is! WaterfallChartData) {
+ area.data = new WaterfallChartData(columns, rows);
+ } else if (rendererSelect.value != "waterfall-chart" &&
+ area.data is WaterfallChartData) {
+ area.data = new ChartData(columns, rows);
+ }
+ activeSeries.renderer = getRendererForType(rendererSelect.value);
+ });
+
+
+ /*
+ * Column selection on active series
+ */
+
+ updateColumns([Event e]) {
+ if (activeSeries == null) return;
+
+ List<Element> buttons = querySelectorAll('.column-button');
+ InputElement firstChecked;
+ for (int i = 0; i < buttons.length && firstChecked == null; i++) {
+ if ((buttons[i] as InputElement).checked) firstChecked = buttons[i];
+ }
+
+ List measures = activeSeries.measures as List;
+ int index = buttons.indexOf(e.target) + 1;
+ if ((e.target as InputElement).checked) {
+ measures.add(index);
+ buttons.forEach((InputElement b) => b.disabled = false);
+ } else {
+ measures.remove(index);
+ if (measures.length <= 1) firstChecked.disabled = true;
+ }
+ }
+
+ updateColumnsList() {
+ columnButtons.children.clear();
+ SMALL_DATA_COLUMNS.asMap().forEach((int index, ChartColumnSpec spec) {
+ if (index == 0) return;
+ var row = new DivElement();
+ var button = new InputElement()
+ ..className = 'column-button'
+ ..type = 'checkbox'
+ ..value = spec.label
+ ..id = 'column-$index'
+ ..onChange.listen((e) => updateColumns(e));
+ var label = new LabelElement()
+ ..text = spec.label
+ ..htmlFor = 'column-$index';
+ row.children.addAll([button,label]);
+ columnButtons.append(row);
+ });
+ }
+
+
+ /*
+ * Updates to series
+ */
+
+ updateSeriesButtonStates() {
+ removeSeriesButton.disabled = seriesList.length <= 1;
+ }
+
+ addSeriesButton.onClick.listen((_) {
+ var name = 'New Series ${customSeriesCounter}',
+ series = new ChartSeries(name, new ObservableList.from([1, 2, 3]),
+ new BarChartRenderer());
+ seriesList.add(series);
+ updateSeriesSelector(series);
+ seriesSelect.selectedIndex = seriesList.length - 1;
+ updateSeriesButtonStates();
+ customSeriesCounter++;
+ });
+
+ removeSeriesButton.onClick.listen((_) {
+ if (seriesList.length > 0) seriesList.removeLast();
+ updateSeriesSelector();
+ updateSeriesButtonStates();
+ });
+
+
+ /*
+ * Update UI
+ */
+
+ updateColumnsList();
+ updateRendererSelector();
+ updateSeriesSelector();
+}
diff --git a/pub/charted/web/renderers_demo.dart b/pub/charted/web/renderers_demo.dart
new file mode 100644
index 0000000..3d70bd2
--- /dev/null
+++ b/pub/charted/web/renderers_demo.dart
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+library charted.demo.renderers;
+
+import 'dart:html';
+import 'package:charted/charts/charts.dart';
+
+import 'charts_demo.dart';
+
+main() {
+ var data = new ChartData(SMALL_DATA_COLUMNS, SMALL_DATA);
+ var dataWaterfallWithSum = new WaterfallChartData(
+ SMALL_WATERFALL_DATA_COLUMNS, SMALL_WATERFALL_DATA_WITH_SUM, [0, 2, 5]);
+
+ // Bar Chart
+ var bar_series = new ChartSeries("one", [2, 3], new BarChartRenderer()),
+ bar_config = new ChartConfig([bar_series], [0]),
+ bar_demo = new ChartDemo(
+ 'Bar chart', querySelector('.bar-chart'), bar_config, data);
+ bar_demo.draw();
+
+ // Line chart
+ var line_series = new ChartSeries("one", [2, 3], new LineChartRenderer()),
+ line_config = new ChartConfig([line_series], [0]),
+ line_demo = new ChartDemo(
+ 'Line chart', querySelector('.line-chart'), line_config, data);
+ line_demo.draw();
+
+ // Stacked bar chart
+ var stacked_series =
+ new ChartSeries("one", [2, 3], new StackedBarChartRenderer()),
+ stacked_config = new ChartConfig([stacked_series], [0]),
+ stacked_demo = new ChartDemo('Stacked bar chart',
+ querySelector('.stacked-bar-chart'), stacked_config, data);
+ stacked_demo.draw();
+
+ // Waterfall Chart
+ var waterfall_series = new ChartSeries("one", [1, 2],
+ new WaterfallChartRenderer()),
+ waterfall_config = new ChartConfig([waterfall_series], [0]),
+ waterfall_demo = new ChartDemo(
+ 'Waterfall chart', querySelector('.waterfall-chart'),
+ waterfall_config, dataWaterfallWithSum);
+ waterfall_demo.draw();
+
+ // Combo chart
+ var combo_bar_series = new ChartSeries("one", [2, 3], new BarChartRenderer()),
+ combo_line_series = new ChartSeries("two", [1], new LineChartRenderer()),
+ combo_config =
+ new ChartConfig([combo_bar_series, combo_line_series], [0]),
+ combo_demo = new ChartDemo(
+ 'Combo chart', querySelector('.combo-chart'), combo_config, data);
+ combo_demo.draw();
+
+ // Pie chart
+ var pie_data = new ChartData(SMALL_DATA_COLUMNS, SMALL_DATA.sublist(0, 1)),
+ pie_series = new ChartSeries("one", [1, 2, 3], new PieChartRenderer()),
+ pie_config = new ChartConfig([pie_series], [0]),
+ pie_demo = new ChartDemo('Pie chart with single row',
+ querySelector('.pie-chart'), pie_config, pie_data,
+ dimensionAxesCount: 0);
+ pie_demo.draw();
+
+ // Pie chart with multiple rows
+ var multi_pie_data =
+ new ChartData(SMALL_DATA_COLUMNS, SMALL_DATA.sublist(0, 3)),
+ multi_pie_series =
+ new ChartSeries("one", [1, 2, 3], new PieChartRenderer()),
+ multi_pie_config = new ChartConfig([multi_pie_series], [0]),
+ multi_pie_demo = new ChartDemo('Pie chart with multiple row',
+ querySelector('.multi-pie-chart'), multi_pie_config, multi_pie_data,
+ dimensionAxesCount: 0);
+ multi_pie_demo.draw();
+}
diff --git a/pub/charted/web/src/dataset_large.dart b/pub/charted/web/src/dataset_large.dart
new file mode 100644
index 0000000..25e2107
--- /dev/null
+++ b/pub/charted/web/src/dataset_large.dart
@@ -0,0 +1,2 @@
+
+part of charted.demo;
\ No newline at end of file
diff --git a/pub/charted/web/src/dataset_small.dart b/pub/charted/web/src/dataset_small.dart
new file mode 100644
index 0000000..12032a4
--- /dev/null
+++ b/pub/charted/web/src/dataset_small.dart
@@ -0,0 +1,41 @@
+
+part of charted.demo;
+
+List SMALL_DATA_COLUMNS = [
+ new ChartColumnSpec(label: 'Month', type: ChartColumnSpec.TYPE_STRING),
+ new ChartColumnSpec(label: 'Precipitation'),
+ new ChartColumnSpec(label: 'High Temperature'),
+ new ChartColumnSpec(label: 'Low Temperature'),
+];
+
+List SMALL_DATA = const [
+ const ['January', 4.50, 7, 6],
+ const ['February', 5.61, 16, 8],
+ const ['March', 8.26, 36, 9],
+ const ['April', 15.46, 63, 49],
+ const ['May', 18.50, 77, 46],
+ const ['June', 14.61, 60, 8],
+ const ['July', 3.26, 9, 6],
+ const ['August', 1.46, 9, 3],
+ const ['September', 1.46, 13, 9],
+ const ['October', 2.46, 29, 3],
+ const ['November', 4.46, 33, 9],
+ const ['December', 8.46, 19, 3]
+];
+
+List SMALL_WATERFALL_DATA_COLUMNS = [
+ new ChartColumnSpec(label: 'Category', type: ChartColumnSpec.TYPE_STRING),
+ new ChartColumnSpec(label: 'Quater 1'),
+ new ChartColumnSpec(label: 'Quater 2'),
+ new ChartColumnSpec(label: 'Quater 3'),
+ new ChartColumnSpec(label: 'Quater 4'),
+];
+
+List SMALL_WATERFALL_DATA_WITH_SUM = const [
+ const ['Product Revenue', 4200, 6000, 6000, 7000],
+ const ['Service Revenue', 2100, 1600, 3000, 4000],
+ const ['Revenue Subtotal', 6300, 7600, 9000, 11000],
+ const ['Fixed Costs', -1700, -1700, -1700, -1700],
+ const ['Viriable Costs', -1000, -2000, -3000, -4500],
+ const ['Gross Earning', 3600, 3900, 4300, 4800],
+];
diff --git a/pub/charted/web/transitions_demo.dart b/pub/charted/web/transitions_demo.dart
new file mode 100644
index 0000000..c4235f8
--- /dev/null
+++ b/pub/charted/web/transitions_demo.dart
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+library charted.demos.transitions;
+
+import 'package:charted/charted.dart';
+
+void main() {
+ SelectionScope scope = new SelectionScope.selector('.wrapper');
+
+
+ Selection svg = scope.append('svg:svg')
+ ..style('width', '4000')
+ ..style('height', '4000');
+
+
+ Selection g1 = svg.append('g');
+ g1.attr('transform', 'translate(30, 30)');
+
+ SvgLine line = new SvgLine();
+
+ List lineData = [
+ [ [0, 10], [400, 10] ],
+ [ [0, 50], [400, 50] ],
+ [ [0, 90], [400, 90] ],
+ [ [0, 130], [400, 130] ]
+ ];
+ DataSelection lines = g1.selectAll('.line').data(lineData);
+
+ lines.enter.append('path');
+ lines
+ ..attrWithCallback('d', (d, i, e) => line.path(d, i, e))
+ ..classed('line');
+ lines.exit.remove();
+
+
+ var text1 = g1.append('text');
+ text1..attr('x', 0)
+ ..attr('y', 0)
+ ..text('Transition cubic-in-out');
+ var text2 = g1.append('text');
+ text2..attr('x', 0)
+ ..attr('y', 40)
+ ..text('Transition cubic-in');
+ var text3 = g1.append('text');
+ text3..attr('x', 0)
+ ..attr('y', 80)
+ ..text('Transition cubic-out');
+ var text4 = g1.append('text');
+ text4..attr('x', 0)
+ ..attr('y', 120)
+ ..text('Transition cubic-out-in');
+
+ var circle = g1.append('circle');
+ circle
+ ..attr('cx', 0)
+ ..attr('cy', 10)
+ ..attr('r', 4)
+ ..classed('circle');
+
+ // Doing transition for first circle with sub transition selection.
+ Transition t1 = new Transition(g1);
+ t1.duration(4000);
+ t1.select('.circle').attr('cx', 400);
+
+ var circle2 = g1.append('circle');
+ circle2
+ ..attr('cx', 0)
+ ..attr('cy', 50)
+ ..attr('r', 4);
+
+ Transition t2 = new Transition(circle2);
+ t2
+ ..ease = clampEasingFn(identityFunction(easeCubic()))
+ ..attr('cx', 400)
+ ..duration(4000);
+
+ var circle3 = g1.append('circle');
+ circle3
+ ..attr('cx', 0)
+ ..attr('cy', 90)
+ ..attr('r', 4);
+
+ Transition t3 = new Transition(circle3);
+ t3
+ ..ease = clampEasingFn(reverseEasingFn(easeCubic()))
+ ..attr('cx', 400)
+ ..duration(4000);
+
+ var circle4 = g1.append('circle');
+ circle4
+ ..attr('cx', 0)
+ ..attr('cy', 130)
+ ..attr('r', 4);
+
+ Transition t4 = new Transition(circle4);
+ t4
+ ..ease = clampEasingFn(reflectReverseEasingFn(easeCubic()))
+ ..attr('cx', 400)
+ ..duration(4000);
+
+
+ Color dotColor1 = new Color.fromRgb(10, 255, 0);
+ Color dotColor2 = new Color.fromRgb(40, 0, 255);
+ String shape1 = 'M50 100 L50 200 L100 200 Z';
+ String shape2 = 'M900 0 L750 200 L900 200 Z';
+
+ var stringInterpolator = interpolateString(shape1, shape2);
+
+ Selection g2 = svg.append('g');
+
+ var text5 = g2.append('text');
+ text5..attr('x', 0)
+ ..attr('y', 0)
+ ..text('Transition shape and color');
+
+ g2.attr('transform', 'translate(30, 200)');
+ g2.attr('width', '1000');
+ g2.attr('height', '400');
+ Selection shape = g2.append('path');
+ shape
+ ..attr('d', shape1)
+ ..attr('fill', dotColor1);
+
+ Transition t5 = new Transition(shape);
+ t5.duration(4000);
+ t5.attr('d', shape2);
+ t5.attr('fill', dotColor2);
+
+ Selection g3 = svg.append('g');
+
+ var text6 = g3.append('text');
+ text6..attr('x', 0)
+ ..attr('y', 0)
+ ..text('Transition delay');
+
+ g3.attr('transform', 'translate(30, 450)');
+ g3.attr('width', '1000');
+ g3.attr('height', '1000');
+
+ Selection rect = g3.append('rect');
+ rect
+ ..attr('x', 0)
+ ..attr('y', 10)
+ ..attr('width', 40)
+ ..attr('height', 40)
+ ..attr('fill', '#FF0000');
+
+ Transition t6 = new Transition(rect);
+ t6.duration(4000);
+ t6.delay(4000);
+ t6.attr('x', 400);
+ t6.attr('fill', '#00FF00');
+
+ var t7 = t6.transition();
+ t7.attr('y', 400);
+ t7.attr('fill', '#0000FF');
+
+ var t8 = t7.transition();
+ t8.attr('x', 0);
+ t8.attr('fill', '#FF00FF');
+
+ var t9 = t8.transition();
+ t9.attr('y', 10);
+ t9.attr('fill', '#FF0000');
+
+ Selection g4 = svg.append('g');
+ DataSelection bars =
+ g4.selectAll('bar').data([120, 160, 210, 260]);
+ g4.attr('transform', 'translate(30, 850)');
+ bars.enter.append('rect');
+ bars
+ ..attrWithCallback('x', (d,i,e) => i * 150)
+ ..attr('y', 350)
+ ..attr('width', 100)
+ ..attr('height', 0)
+ ..style('fill', '#6699FF');
+ bars.exit.remove();
+
+ var t = bars.transition();
+ t.duration(1000);
+ t.delayWithCallback((d, i, c) => i * 200);
+ t.attrTween('y', (d, i, attr) => interpolateString(
+ attr, (350 - d).toString()));
+ t.attrTween('height', (d, i, attr) => interpolateString(attr, d.toString()));
+
+ var color = t.transition();
+ color.delayWithCallback((d, i, c) => i * 200);
+ color.styleTween('fill',
+ (d, i, style) => interpolateString(style, '#CC0088'));
+
+ Color hslColor1 = new Color.fromRgb(10, 255, 0);
+ Color hslColor2 = new Color.fromRgb(40, 0, 255);
+ Selection g5 = svg.append('g');
+ var text7 = g5.append('text');
+ text7..attr('x', 0)
+ ..attr('y', 0)
+ ..text('HSL Color Transition with Transform');
+
+ g5.attr('transform', 'translate(30, 1300)');
+
+ Selection rect2 = g5.append('rect');
+ rect2
+ ..attr('x', 0)
+ ..attr('y', 10)
+ ..attr('width', 200)
+ ..attr('height', 60);
+ rect2.transition()
+ ..attrTween('fill', (d, i, e) {
+ return interpolateHsl(hslColor1, hslColor2);})
+ ..duration(2000);
+
+ rect2.transition()
+ ..attrTween('transform', (d, i, e) => interpolateTransform(
+ "translate(10,10)rotate(30)skewX(0.5)scale(1,1)",
+ "translate(100,100)rotate(360)skewX(45)scale(3,3)"))
+ ..duration(5000);
+
+}
+
diff --git a/pub/code_transformers/.analysis_options b/pub/code_transformers/.analysis_options
new file mode 100644
index 0000000..a10d4c5
--- /dev/null
+++ b/pub/code_transformers/.analysis_options
@@ -0,0 +1,2 @@
+analyzer:
+ strong-mode: true
diff --git a/pub/code_transformers/.gitignore b/pub/code_transformers/.gitignore
new file mode 100644
index 0000000..f20a0d0
--- /dev/null
+++ b/pub/code_transformers/.gitignore
@@ -0,0 +1,18 @@
+# Don’t commit the following directories created by pub.
+.pub
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.dart.precompiled.js
+*.js_
+*.js.deps
+*.js.map
+*.sw?
+.idea/
+.pub/
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/pub/code_transformers/.test_config b/pub/code_transformers/.test_config
new file mode 100644
index 0000000..2535563
--- /dev/null
+++ b/pub/code_transformers/.test_config
@@ -0,0 +1,3 @@
+{
+ "test_package": true
+}
diff --git a/pub/code_transformers/.travis.yml b/pub/code_transformers/.travis.yml
new file mode 100644
index 0000000..2331ace
--- /dev/null
+++ b/pub/code_transformers/.travis.yml
@@ -0,0 +1,9 @@
+language: dart
+sudo: false
+dart:
+ - dev
+ - stable
+cache:
+ directories:
+ - $HOME/.pub-cache/hosted
+script: ./tool/travis.sh
diff --git a/pub/code_transformers/AUTHORS b/pub/code_transformers/AUTHORS
new file mode 100644
index 0000000..0617765
--- /dev/null
+++ b/pub/code_transformers/AUTHORS
@@ -0,0 +1,9 @@
+# Names should be added to this file with this pattern:
+#
+# For individuals:
+# Name <email address>
+#
+# For organizations:
+# Organization <fnmatch pattern>
+#
+Google Inc. <*@google.com>
diff --git a/pub/code_transformers/BUILD.gn b/pub/code_transformers/BUILD.gn
new file mode 100644
index 0000000..cabfd56
--- /dev/null
+++ b/pub/code_transformers/BUILD.gn
@@ -0,0 +1,20 @@
+# This file is generated by importer.py for code_transformers-0.4.2+4
+
+import("//build/dart/dart_package.gni")
+
+dart_package("code_transformers") {
+ package_name = "code_transformers"
+
+ source_dir = "lib"
+
+ disable_analysis = true
+
+ deps = [
+ "//third_party/dart-pkg/pub/source_maps",
+ "//third_party/dart-pkg/pub/barback",
+ "//third_party/dart-pkg/pub/source_span",
+ "//third_party/dart-pkg/pub/cli_util",
+ "//third_party/dart-pkg/pub/path",
+ "//dart/pkg/analyzer",
+ ]
+}
diff --git a/pub/code_transformers/CHANGELOG.md b/pub/code_transformers/CHANGELOG.md
new file mode 100644
index 0000000..f14a303
--- /dev/null
+++ b/pub/code_transformers/CHANGELOG.md
@@ -0,0 +1,147 @@
+## 0.4.2+4
+
+* Update to work with analyzer 0.29.x and transform_test 0.2.x
+
+## 0.4.2+3
+
+* Update to work with analyzer 0.28.x.
+
+## 0.4.2+2
+
+* Update to work with analyzer 0.23.x.
+
+## 0.4.2+1
+
+* Contains a fix for the `useSharedSources` option that could result in null
+ library elements when running on multiple entry points.
+
+## 0.4.2
+
+* Use Strong Mode, fixes
+ [#38](https://github.com/dart-lang/code_transformers/issues/38).
+
+## 0.4.1
+
+* Added a fix for [#24890](https://github.com/dart-lang/sdk/issues/24890).
+ * All constants in all libraries will once again be resolved by default.
+ * Added a new `resolveAllLibraries` option to `Resolver#resolve` and
+ `Resolvers#get`. If `false` is passed then constants will not be resolved in
+ non entry points. This saves significant time if constants are not needed.
+* Added a `useSharedSources` option to `Resolvers`. This gives a significant
+ speed increase, but must be used carefully.
+ * If used, then all `Resolver` instances created from the same `Resolvers`
+ instance will share the same sources cache.
+ * This should be generally safe to use if the `Resolvers` instance is created
+ in the constructor of your `Transformer`.
+ * This option should probably not be used with a static or shared `Resolvers`
+ instance.
+
+## 0.4.0
+
+* Remove dependency on `test`, and move all test related apis to a new
+ `transformer_test` package which is now a dev dependency.
+
+## 0.3.1
+
+* Update to analyzer `>=0.27.0 <0.28.0`.
+
+## 0.3.0+1
+
+* Upgrade `test` to a real dependency.
+
+## 0.3.0
+
+* Re-release `0.2.10` release as `0.3.0`.
+
+## 0.2.11
+
+* Revert `0.2.10` release, will be re-released as `0.3.0` since it is actually
+ a breaking change.
+
+## 0.2.10
+
+* Update to use the `test` package instead of the `unittest` package.
+
+## 0.2.9+4
+
+* Republish 0.2.9+2 under new version.
+
+## 0.2.9+3
+
+* Republish of 0.2.9 to ensure nobody gets 0.2.9+1 in the future.
+
+## 0.2.9+2
+
+* Update to analyzer '>=0.26.0 <0.27.0'
+
+## 0.2.9+1
+
+* Update to analyzer '<0.27.0'
+* This version will be reverted as it wasn't compatible with <0.26.0.
+
+## 0.2.9
+
+* Update to analyzer `<=0.26.0`.
+
+## 0.2.8
+
+* Add `benchmarks.dart` file which exposes a `TransformerBenchmark`. This can be
+used to implement simple benchmarks of transformer code.
+
+## 0.2.7+2
+
+* Fix `assetIdToUri` on windows,
+[41](https://github.com/dart-lang/polymer-dart/issues/41)
+
+## 0.2.7+1
+
+* Fixes for missing overrides after upgrade to analzyer 0.24.0
+
+## 0.2.7
+
+* Added default set of mockSdkSources and upgraded to analyzer 0.24.0
+
+## 0.2.6
+
+* Added `assetIdToUri` to assets.dart.
+
+## 0.2.5
+* Improvements to `dartSdkDirectory` so it has a better chance of success.
+* `BuildLogger` now accepts `AggregateTransform` or `Transform`. If passing in
+an `AggregateTransform` you must also pass in an `AssetId` to use as the primary
+input.
+
+## 0.2.4
+
+* Added some basic string formatting options to `testPhases` to make it a bit
+ less strict if desired.
+
+## 0.2.3+2
+
+* Added logic to discover the location of the dart SDK when the dart binary is a
+ symlink.
+
+## 0.2.3
+
+* Added support for logging stable error messages from transformers.
+
+## 0.2.2
+
+* Added two transformers, 'delete_file' and 'remove_sourcemap_comment'.
+
+## 0.2.0+3
+
+* Raise the lower bound on the source_maps constraint to exclude incompatible
+ versions.
+
+## 0.2.0+2
+
+* Widen the constraint on source_maps.
+
+## 0.2.0+1
+
+* Widen the constraint on barback.
+
+## 0.2.0
+
+* Switch from `source_maps`' `Span` class to `source_span`'s `SourceSpan` class.
diff --git a/pub/code_transformers/LICENSE b/pub/code_transformers/LICENSE
new file mode 100644
index 0000000..5c60afe
--- /dev/null
+++ b/pub/code_transformers/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2014, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pub/code_transformers/PATENTS b/pub/code_transformers/PATENTS
new file mode 100644
index 0000000..6954196
--- /dev/null
+++ b/pub/code_transformers/PATENTS
@@ -0,0 +1,23 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Dart Project.
+
+Google hereby grants to you a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this
+section) patent license to make, have made, use, offer to sell, sell,
+import, transfer, and otherwise run, modify and propagate the contents
+of this implementation of Dart, where such license applies only to
+those patent claims, both currently owned by Google and acquired in
+the future, licensable by Google that are necessarily infringed by
+this implementation of Dart. This grant does not include claims that
+would be infringed only as a consequence of further modification of
+this implementation. If you or your agent or exclusive licensee
+institute or order or agree to the institution of patent litigation
+against any entity (including a cross-claim or counterclaim in a
+lawsuit) alleging that this implementation of Dart or any code
+incorporated within this implementation of Dart constitutes direct or
+contributory patent infringement, or inducement of patent
+infringement, then any patent rights granted to you under this License
+for this implementation of Dart shall terminate as of the date such
+litigation is filed.
diff --git a/pub/code_transformers/README.md b/pub/code_transformers/README.md
new file mode 100644
index 0000000..758d205
--- /dev/null
+++ b/pub/code_transformers/README.md
@@ -0,0 +1,75 @@
+# Code Transformers
+This package exposes various tools to help in the creation and testing of
+barback transformers, as well as an interface for logging messages with
+constant ids for documentation purposes.
+
+## Messages and logging
+
+This package exposes a `BuildLogger` class as well as `Message`,
+`MessageTemplate`, and `MessageId` classes. These work together to provide
+stable error messages with ids that can be referenced in documentation.
+
+### MessageId
+
+A `MessageId` is a constant definition of a message in a package. These are
+used to group messages with the same id or link to sections in a document.
+
+ const myId = const MessageId('myPackage', 0);
+
+These ids should typically never change or disappear throughout the entire
+lifetime of a package.
+
+### Message
+
+A `Message` is a const object which has a `MessageId` and `snippet`.
+
+ const myMessage = const Message(myId, 'my message');
+
+### MessageTemplate
+
+TODO(jakemac): Docs on this, see
+https://github.com/dart-lang/code-transformers/blob/master/lib/messages/messages.dart
+
+### BuildLogger
+
+The `BuildLogger` class just wraps a normal `TransformLogger` to provide some
+additional functionality. You use it in the same way as a `TransformLogger`
+except that the log methods can accept a `String` or a `Message`. This should
+usually be created in the first step of your transformers `apply` function:
+
+ apply(Transform transform) {
+ // If detailsUri is passed, then a link will be output with each log
+ // message that follows this format
+ // `$detailsUri#${msg.id.package}_${msg.id.id}`.
+ var logger = new BuildLogger(transform, detailsUri: 'http://foo.com');
+ }
+
+You can optionally dump out a file containing all the logs found in JSON
+format by calling the `writeOutput` method on the logger when you are done.
+The output file will have the same file path as the primary input of the
+transformer, with an extension matching this pattern `._buildLogs.$i`. The
+`i` increments starting at 0 each time that logs are output (if multiple
+transformers run on the same file for instance). These can all be combined
+into a single file later on by calling the static
+`combineLogFiles(Transform transform)` function.
+
+## Testing Transformers
+
+TODO(jakemac): Docs on this, see `testPhases` in
+https://github.com/dart-lang/code-transformers/blob/master/lib/tests.dart
+
+## Using the Analyzer in Transformers
+
+This package exposes a `Resolver` class which helps out when using the
+analyzer in a transform.
+
+TODO(jakemac): Docs on this, see
+https://github.com/dart-lang/code-transformers/blob/master/lib/src/resolver.dart
+
+## Barback AssetIds and Uris
+
+This package also provides some helpers to convert `AssetId`s to and from `Uri`s
+relative to a source asset.
+
+TODO(jakemac): Docs on this, see `uriToAssetId` & `assetIdToUri` in
+https://github.com/dart-lang/code-transformers/blob/master/lib/assets.dart
diff --git a/pub/code_transformers/codereview.settings b/pub/code_transformers/codereview.settings
new file mode 100644
index 0000000..4cda648
--- /dev/null
+++ b/pub/code_transformers/codereview.settings
@@ -0,0 +1,3 @@
+CODE_REVIEW_SERVER: http://codereview.chromium.org/
+VIEW_VC: https://github.com/dart-lang/code-transformers/commit/
+CC_LIST: reviews@dartlang.org
diff --git a/pub/code_transformers/lib/assets.dart b/pub/code_transformers/lib/assets.dart
new file mode 100644
index 0000000..dacc582
--- /dev/null
+++ b/pub/code_transformers/lib/assets.dart
@@ -0,0 +1,151 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Common methods used by transfomers for dealing with asset IDs.
+library code_transformers.assets;
+
+import 'dart:math' show min, max;
+
+import 'package:barback/barback.dart';
+import 'package:path/path.dart' as path;
+import 'package:source_span/source_span.dart';
+
+import 'messages/build_logger.dart';
+import 'src/messages.dart';
+
+/// Create an [AssetId] for a [url] seen in the [source] asset.
+///
+/// By default this is used to resolve relative urls that occur in HTML assets,
+/// including cross-package urls of the form "packages/foo/bar.html". Dart
+/// "package:" urls are not resolved unless [source] is Dart file (has a .dart
+/// extension).
+///
+/// This function returns null if [url] can't be resolved. We log a warning
+/// message in [logger] if we know the reason why resolution failed. This
+/// happens, for example, when using incorrect paths to reach into another
+/// package, or when [errorsOnAbsolute] is true and the url seems to be
+/// absolute. If [span] is not `null` it is used to provide context for any
+/// warning message(s) generated.
+// TODO(sigmund): delete once this is part of barback (dartbug.com/12610)
+AssetId uriToAssetId(
+ AssetId source, String url, TransformLogger logger, SourceSpan span,
+ {bool errorOnAbsolute: true}) {
+ if (url == null || url == '') return null;
+ var uri = Uri.parse(url);
+ var urlBuilder = path.url;
+ if (uri.host != '' || uri.scheme != '' || urlBuilder.isAbsolute(url)) {
+ if (source.extension == '.dart' && uri.scheme == 'package') {
+ var index = uri.path.indexOf('/');
+ if (index != -1) {
+ return new AssetId(
+ uri.path.substring(0, index), 'lib${uri.path.substring(index)}');
+ }
+ }
+
+ if (errorOnAbsolute) {
+ var msg = NO_ABSOLUTE_PATHS.create({'url': url});
+ logger.warning(logger is BuildLogger ? msg : msg.snippet, span: span);
+ }
+ return null;
+ }
+
+ var targetPath = urlBuilder
+ .normalize(urlBuilder.join(urlBuilder.dirname(source.path), url));
+ var segments = urlBuilder.split(targetPath);
+ var sourceSegments = urlBuilder.split(source.path);
+ assert(sourceSegments.length > 0);
+ var topFolder = sourceSegments[0];
+ var entryFolder = topFolder != 'lib' && topFolder != 'asset';
+
+ // Find the first 'packages/' or 'assets/' segment:
+ var packagesIndex = segments.indexOf('packages');
+ var assetsIndex = segments.indexOf('assets');
+ var index = (packagesIndex >= 0 && assetsIndex >= 0)
+ ? min(packagesIndex, assetsIndex)
+ : max(packagesIndex, assetsIndex);
+ if (index > -1) {
+ if (entryFolder) {
+ // URLs of the form "packages/foo/bar" seen under entry folders (like
+ // web/, test/, example/, etc) are resolved as an asset in another
+ // package. 'packages' can be used anywhere, there is no need to walk up
+ // where the entrypoint file was.
+ // TODO(sigmund): this needs to change: Only resolve when index == 1 &&
+ // topFolder == segment[0], otherwise give a warning (dartbug.com/17596).
+ return _extractOtherPackageId(index, segments, logger, span);
+ } else if (index == 1 && segments[0] == '..') {
+ // Relative URLs of the form "../../packages/foo/bar" in an asset under
+ // lib/ or asset/ are also resolved as an asset in another package, but we
+ // check that the relative path goes all the way out where the packages
+ // folder lives (otherwise the app would not work in Dartium). Since
+ // [targetPath] has been normalized, "packages" or "assets" should be at
+ // index 1.
+ return _extractOtherPackageId(1, segments, logger, span);
+ } else {
+ var prefix = segments[index];
+ var fixedSegments = <String>[];
+ fixedSegments.addAll(sourceSegments.map((_) => '..'));
+ fixedSegments.addAll(segments.sublist(index));
+ var fixedUrl = urlBuilder.joinAll(fixedSegments);
+ var msg = INVALID_URL_TO_OTHER_PACKAGE
+ .create({'url': url, 'prefix': prefix, 'fixedUrl': fixedUrl});
+ logger.warning(logger is BuildLogger ? msg : msg.snippet, span: span);
+ return null;
+ }
+ }
+
+ // Otherwise, resolve as a path in the same package.
+ return new AssetId(source.package, targetPath);
+}
+
+AssetId _extractOtherPackageId(
+ int index, List<String> segments, TransformLogger logger, SourceSpan span) {
+ if (index >= segments.length) return null;
+ var prefix = segments[index];
+ if (prefix != 'packages' && prefix != 'assets') return null;
+ var folder = prefix == 'packages' ? 'lib' : 'asset';
+ if (segments.length < index + 3) {
+ var msg = INVALID_PREFIX_PATH.create({'prefix': prefix, 'folder': folder});
+ logger.warning(logger is BuildLogger ? msg : msg.snippet, span: span);
+ return null;
+ }
+ return new AssetId(segments[index + 1],
+ path.url.join(folder, path.url.joinAll(segments.sublist(index + 2))));
+}
+
+/// Gets a URI which would be appropriate for importing the file represented by
+/// [assetId].
+///
+/// This function returns null if we cannot determine a uri for [assetId].
+///
+/// Note that [assetId] may represent a non-importable file such as a part.
+String assetIdToUri(AssetId assetId,
+ {TransformLogger logger, SourceSpan span, AssetId from}) {
+ if (!assetId.path.startsWith('lib/')) {
+ // Cannot do absolute imports of non lib-based assets.
+ if (from == null) {
+ if (logger != null) {
+ var msg = UNSPECIFIED_FROM_IN_NON_LIB_ASSET.create({'id': '$assetId'});
+ logger.warning(logger is BuildLogger ? msg : msg.snippet, span: span);
+ }
+ return null;
+ }
+
+ if (assetId.package != from.package) {
+ if (logger != null) {
+ var msg = IMPORT_FROM_DIFFERENT_PACKAGE
+ .create({'toId': '$assetId', 'fromId': '$from'});
+ logger.warning(logger is BuildLogger ? msg : msg.snippet, span: span);
+ }
+ return null;
+ }
+ return new Uri(
+ path: path.url
+ .relative(assetId.path, from: path.url.dirname(from.path)))
+ .toString();
+ }
+
+ return Uri
+ .parse('package:${assetId.package}/${assetId.path.substring(4)}')
+ .toString();
+}
diff --git a/pub/code_transformers/lib/benchmarks.dart b/pub/code_transformers/lib/benchmarks.dart
new file mode 100644
index 0000000..c048457
--- /dev/null
+++ b/pub/code_transformers/lib/benchmarks.dart
@@ -0,0 +1,76 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library code_transformers.benchmarks;
+
+import 'dart:async';
+import 'package:barback/barback.dart';
+import 'src/async_benchmark_base.dart';
+
+/// A benchmark for testing the performance of transformer phases.
+class TransformerBenchmark extends AsyncBenchmarkBase {
+ /// Internal abstraction layer for barback.
+ _BenchmarkHelper _helper;
+
+ /// The transformer phases to be ran.
+ final List<List<Transformer>> transformers;
+
+ /// The files to pass to barback.
+ final Map<AssetId, String> files;
+
+ TransformerBenchmark(this.transformers, this.files);
+
+ @override
+ Future setup() {
+ _helper = new _BenchmarkHelper(transformers, files);
+ return new Future.value();
+ }
+
+ @override
+ Future run() => _helper.run();
+
+ @override
+ teardown() {
+ _helper = null;
+ return new Future.value();
+ }
+}
+
+/// Barback abstraction layer.
+class _BenchmarkHelper implements PackageProvider {
+ /// All the files available.
+ final Map<AssetId, String> _files;
+
+ /// All the packages.
+ final Iterable<String> packages;
+
+ /// Internal instance of barback.
+ Barback _barback;
+
+ /// Subscription to barback results.
+ StreamSubscription<BuildResult> resultSubscription;
+
+ _BenchmarkHelper(
+ List<List<Transformer>> transformers, Map<AssetId, String> files)
+ : _files = files,
+ packages = new Set.from(files.keys.map((assetId) => assetId.package)) {
+ _barback = new Barback(this);
+ for (var package in packages) {
+ _barback.updateTransformers(package, transformers);
+ }
+ }
+
+ /// Look up an [AssetId] in [files] and return an [Asset] for it.
+ @override
+ Future<Asset> getAsset(AssetId id) =>
+ new Future.value(new Asset.fromString(id, _files[id]));
+
+ /// Tells barback which files have changed, and thus anything that depends on
+ /// it on should be computed. Returns a [Future] that completes once some
+ /// results are received.
+ Future run([Iterable<AssetId> assetIds]) {
+ if (assetIds == null) assetIds = _files.keys;
+ _barback.updateSources(assetIds);
+ return _barback.results.first;
+ }
+}
diff --git a/pub/code_transformers/lib/messages/build_logger.dart b/pub/code_transformers/lib/messages/build_logger.dart
new file mode 100644
index 0000000..39dfcf8
--- /dev/null
+++ b/pub/code_transformers/lib/messages/build_logger.dart
@@ -0,0 +1,155 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library code_transformers.messages.messages_logger;
+
+import 'dart:async';
+import 'dart:convert' show JSON;
+
+import 'package:barback/barback.dart';
+import 'package:source_span/source_span.dart';
+
+import 'messages.dart' show Message, BuildLogEntry, LogEntryTable;
+
+/// A [TransformLogger] used to track error and warning messages produced during
+/// a build.
+///
+/// This logger records all messages that were logged and then forwards
+/// the calls to an underlying [TransformLogger]. The internal records support
+/// serializing the errors and emiting them to an asset (so they can be
+/// presented to the user in a web-based client), clustering similar messages
+/// together, sorting messages in order of importance, etc.
+///
+/// The logger also supports reporting error messages as warnings. Barback makes
+/// error messages stop the transformation process, which sometimes can surprise
+/// users. Turning errors into warnings is especially useful when used within
+/// `pub serve`, where we would like the transformation to continue as far as it
+/// can. When this flag is turned on, the level is still recorded as an error,
+/// so a web client UI can still highlight their importance.
+// TODO(sigmund): also cluster messages when they are reported on the
+// command-line.
+class BuildLogger implements TransformLogger {
+ /// Underling transform that is currently active. This can be either an
+ /// [AggregateTransform] or [Transform].
+ final _transform;
+
+ /// The primary input id.
+ final AssetId _primaryId;
+
+ /// Logs created during the current transform.
+ final LogEntryTable _logs = new LogEntryTable();
+
+ /// Whether to use `warning` or `error` when forwarding error messages to the
+ /// underlying logger in `_transform.logger`.
+ final bool convertErrorsToWarnings;
+
+ /// Uri prefix to link for additional details. If set, messages logged through
+ /// this logger will contain an additional sentence, telling users to find
+ /// more details at `$detailsUri#packagename_id`.
+ final String detailsUri;
+
+ /// If transform is a [Transform] then [primaryId] will default to the
+ /// primaryInput.id, if it is an [AggregateTransform] then you must pass in
+ /// a [primaryId] to be used, otherwise you will get a runtime error.
+ BuildLogger(transform,
+ {this.convertErrorsToWarnings: false, AssetId primaryId, this.detailsUri})
+ : _transform = transform,
+ _primaryId = primaryId != null ? primaryId : transform.primaryInput.id;
+
+ /// Records a message at the fine level. If [msg] is a [Message] it is
+ /// recorded directly, otherwise it is first converted to a [String].
+ void fine(Object msg, {AssetId asset, SourceSpan span}) {
+ msg = msg is Message ? msg : new Message.unknown('$msg');
+ _transform.logger.fine(_snippet(msg), asset: asset, span: span);
+ _logs.add(new BuildLogEntry(msg, span, LogLevel.FINE.name));
+ }
+
+ /// Records a message at the info level. If [msg] is a [Message] it is
+ /// recorded directly, otherwise it is first converted to a [String].
+ void info(Object msg, {AssetId asset, SourceSpan span}) {
+ msg = msg is Message ? msg : new Message.unknown('$msg');
+ _transform.logger.info(_snippet(msg), asset: asset, span: span);
+ _logs.add(new BuildLogEntry(msg, span, LogLevel.INFO.name));
+ }
+
+ /// Records a warning message. If [msg] is a [Message] it is recorded
+ /// directly, otherwise it is first converted to a [String].
+ void warning(Object msg, {AssetId asset, SourceSpan span}) {
+ msg = msg is Message ? msg : new Message.unknown('$msg');
+ _transform.logger.warning(_snippet(msg), asset: asset, span: span);
+ _logs.add(new BuildLogEntry(msg, span, LogLevel.WARNING.name));
+ }
+
+ /// Records an error message. If [msg] is a [Message] it is recorded
+ /// directly, otherwise it is first converted to a [String].
+ void error(Object msg, {AssetId asset, SourceSpan span}) {
+ msg = msg is Message ? msg : new Message.unknown('$msg');
+ if (convertErrorsToWarnings) {
+ _transform.logger.warning(_snippet(msg), asset: asset, span: span);
+ } else {
+ _transform.logger.error(_snippet(msg), asset: asset, span: span);
+ }
+ _logs.add(new BuildLogEntry(msg, span, LogLevel.ERROR.name));
+ }
+
+ String _snippet(Message msg) {
+ var s = msg.snippet;
+ if (detailsUri == null) return s;
+ var dot = s.endsWith('.') || s.endsWith('!') || s.endsWith('?') ? '' : '.';
+ var hashTag = '${msg.id.package}_${msg.id.id}';
+ return '$s$dot See $detailsUri#$hashTag for details.';
+ }
+
+ /// Outputs the log data to a JSON serialized file.
+ Future writeOutput() {
+ return _getNextLogAssetId().then((id) {
+ _transform.addOutput(new Asset.fromString(id, JSON.encode(_logs)));
+ });
+ }
+
+ // Each phase outputs a new log file with an incrementing # appended, this
+ // figures out the next # to use.
+ Future<AssetId> _getNextLogAssetId([int nextNumber = 1]) async {
+ var nextAssetPath = _primaryId.addExtension('${LOG_EXTENSION}.$nextNumber');
+ bool exists = await _transform.hasInput(nextAssetPath);
+ if (!exists) return nextAssetPath;
+ return _getNextLogAssetId(++nextNumber);
+ }
+
+ // Reads all log files for an Asset into [logs].
+ static Future _readLogFilesForAsset(
+ AssetId id, Transform transform, LogEntryTable entries,
+ [nextNumber = 1]) {
+ var nextAssetPath = id.addExtension('${LOG_EXTENSION}.$nextNumber');
+ return transform.hasInput(nextAssetPath).then((exists) {
+ if (!exists) return null;
+ return transform.readInputAsString(nextAssetPath).then((data) {
+ entries.addAll(new LogEntryTable.fromJson(
+ JSON.decode(data) as Map<String, Iterable>));
+ return _readLogFilesForAsset(id, transform, entries, ++nextNumber);
+ });
+ });
+ }
+
+ /// Combines all existing ._buildLogs.* files into a single ._buildLogs file.
+ /// [transform] may be a [Transform] or [AggregateTransform]. If an
+ /// [AggregateTransform] is passed then [primaryId] must also be passed.
+ static Future combineLogFiles(transform, [AssetId primaryId]) {
+ if (primaryId == null) primaryId = transform.primaryInput.id;
+ var entries = new LogEntryTable();
+ return _readLogFilesForAsset(primaryId, transform, entries).then((_) {
+ return transform.addOutput(new Asset.fromString(
+ primaryId.addExtension(LOG_EXTENSION),
+ JSON.encode(entries.toJson())));
+ });
+ }
+
+ // Reads all logs for an asset and adds them to this loggers log output.
+ Future addLogFilesFromAsset(AssetId id, [int nextNumber = 1]) {
+ return _readLogFilesForAsset(id, _transform, _logs);
+ }
+}
+
+/// Extension used for assets that contained serialized logs.
+const String LOG_EXTENSION = '._buildLogs';
diff --git a/pub/code_transformers/lib/messages/messages.dart b/pub/code_transformers/lib/messages/messages.dart
new file mode 100644
index 0000000..77ea05f
--- /dev/null
+++ b/pub/code_transformers/lib/messages/messages.dart
@@ -0,0 +1,235 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Defines messages templates and an adapter for TransformLogger to be able
+/// report error messages from transformers and refer to them in a consistent
+/// manner long term.
+library code_transformers.messages;
+
+// Note: this library purposely doesn't depend on dart:io, dart:html, or barback
+// so it can easily be used both in the transformers and in client-side apps
+// (for example in the log_injector).
+import 'dart:collection' show LinkedHashMap;
+
+import 'package:source_span/source_span.dart';
+
+/// A globally unique identifier for an error message. This identifier should be
+/// stable, that is, it should never change after it is asigned to a particular
+/// message. That allows us to document our error messages and make them
+/// searchable for prosperity.
+class MessageId implements Comparable<MessageId> {
+ /// Name of the package that declares this message.
+ final String package;
+
+ /// Message identifier number, unique within the package.
+ final int id;
+
+ const MessageId(this.package, this.id);
+
+ static const MessageId NOT_SPECIFIED = const MessageId('unknown', 0);
+
+ /// Serialize this message. We use a string and not a map to encode ids so
+ /// they can be used as keys in JSON maps.
+ String toJson() => toString();
+
+ toString() => '${package}#$id';
+
+ int compareTo(MessageId other) {
+ var res = package.compareTo(other.package);
+ if (res != 0) return res;
+ return id.compareTo(other.id);
+ }
+
+ /// Creates a new [MessageId] from an encoded value produced via [toJson].
+ factory MessageId.fromJson(data) {
+ var index = data.lastIndexOf('#');
+ if (index == -1) throw 'Invalid message id: $data';
+ return new MessageId(
+ data.substring(0, index), int.parse(data.substring(index + 1)));
+ }
+
+ operator ==(Object other) =>
+ other is MessageId && package == other.package && id == other.id;
+
+ int get hashCode => 31 * package.hashCode + id;
+}
+
+/// An instance of an error message. These are typically produced from a
+/// [MessageTemplate].
+class Message {
+ /// A globally unique identifier for this message.
+ final MessageId id;
+
+ /// A snippet message that is presented to the user.
+ final String snippet;
+
+ const Message(this.id, this.snippet);
+
+ const Message.unknown(this.snippet) : id = MessageId.NOT_SPECIFIED;
+
+ /// Serializes this message to JSON.
+ Map toJson() => {'id': id.toJson(), 'snippet': snippet};
+ String toString() => 'id: $id, snippet: $snippet';
+
+ /// Creates a new [Message] from an encoded value produced via [toJson].
+ factory Message.fromJson(data) =>
+ new Message(new MessageId.fromJson(data['id']), data['snippet']);
+}
+
+/// Template for a message. Templates can include placeholders to indicate
+/// values that are different for each instance of the error. Calling [create]
+/// will generate the actual message, with the placeholders replaced with
+/// values. If there are no placeholders, an instance of [MessageTemplate] is a
+/// valid instance of [Message] as well.
+class MessageTemplate implements Message {
+ /// Unique and stable id for the message.
+ final MessageId id;
+
+ /// Template message with placeholders of the form `%-name-%`.
+ final String snippetTemplate;
+
+ /// This returns the message snippet, only if it the template has no
+ /// placeholders, otherwise this throws an exception. Most messages have no
+ /// placeholder arguments, in those cases, the snippet can be computed
+ /// without specifying any arguments (exactly like calling `create()` with no
+ /// arguments).
+ String get snippet => _createSnippet();
+
+ /// Short description of the error message, typically used as a title of the
+ /// error message in autogenerated documentation. This should be a single
+ /// phrase, and cannot use placeholders.
+ final String description;
+
+ /// Additional details about this error message. These are used to
+ /// automatically generate documentation.
+ final String details;
+
+ const MessageTemplate(
+ this.id, this.snippetTemplate, this.description, this.details);
+
+ static final _placeholderPattern = new RegExp(r"%-(\w*)-%");
+
+ _createSnippet([Map args = const {}, bool fillUnknowns = false]) {
+ var snippet = snippetTemplate.replaceAllMapped(_placeholderPattern, (m) {
+ var arg = m.group(1);
+ var value = args[arg];
+ if (value != null) return '$value';
+ if (fillUnknowns) return '';
+ throw "missing argument $arg, for error message: $snippetTemplate";
+ });
+ return snippet;
+ }
+
+ create([Map args = const {}, bool fillUnknowns = false]) =>
+ new Message(id, _createSnippet(args, fillUnknowns));
+
+ /// Serializes this message to JSON.
+ Map toJson() => create().toJson();
+ String toString() => '${toJson()}';
+}
+
+/// Represents an actual log entry for a build error message. Including the
+/// actual message, its severity level (warning, error, etc), and a source span
+/// for a code location that is revelant to the message.
+class BuildLogEntry {
+ /// The actual message.
+ final Message message;
+
+ /// Severity level.
+ final String level;
+
+ /// Location associated with this message, if any.
+ final SourceSpan span;
+
+ BuildLogEntry(this.message, this.span, this.level);
+
+ /// Creates a new [BuildLogEntry] from an encoded value produced via [toJson].
+ factory BuildLogEntry.fromJson(Map data) {
+ var spanData = data['span'];
+ var span = null;
+ if (spanData != null) {
+ var locData = spanData['start'];
+ var start = new SourceLocation(locData['offset'],
+ sourceUrl: Uri.parse(locData['url']),
+ line: locData['line'],
+ column: locData['column']);
+ locData = spanData['end'];
+ var end = new SourceLocation(locData['offset'],
+ sourceUrl: Uri.parse(locData['url']),
+ line: locData['line'],
+ column: locData['column']);
+ span = new SourceSpan(start, end, spanData['text']);
+ }
+ return new BuildLogEntry(
+ new Message.fromJson(data['message']), span, data['level']);
+ }
+
+ /// Serializes this log entry to JSON.
+ Map toJson() {
+ var data = {
+ 'level': level,
+ 'message': message.toJson(),
+ };
+ if (span != null) {
+ data['span'] = {
+ 'start': {
+ 'url': span.start.sourceUrl.toString(),
+ 'offset': span.start.offset,
+ 'line': span.start.line,
+ 'column': span.start.column,
+ },
+ 'end': {
+ 'url': span.end.sourceUrl.toString(),
+ 'offset': span.end.offset,
+ 'line': span.end.line,
+ 'column': span.end.column,
+ },
+ 'text': span.text,
+ };
+ }
+ return data;
+ }
+
+ String toString() => '${toJson()}';
+}
+
+/// A table of entries, that clusters error messages by id.
+class LogEntryTable {
+ final Map<MessageId, List<BuildLogEntry>> entries;
+
+ LogEntryTable() : entries = new LinkedHashMap();
+
+ /// Creates a new [LogEntryTable] from an encoded value produced via [toJson].
+ factory LogEntryTable.fromJson(Map<String, Iterable> json) {
+ var res = new LogEntryTable();
+ for (String key in json.keys) {
+ var id = new MessageId.fromJson(key);
+ res.entries[id] =
+ json[key].map((v) => new BuildLogEntry.fromJson(v)).toList();
+ }
+ return res;
+ }
+
+ /// Serializes this entire table as JSON.
+ Map toJson() {
+ var res = {};
+ entries.forEach((key, value) {
+ res['$key'] = value.map((e) => e.toJson()).toList();
+ });
+ return res;
+ }
+
+ String toString() => '${toJson()}';
+
+ void add(BuildLogEntry entry) {
+ entries.putIfAbsent(entry.message.id, () => []).add(entry);
+ }
+
+ void addAll(LogEntryTable other) {
+ for (var key in other.entries.keys) {
+ var values = entries.putIfAbsent(key, () => []);
+ values.addAll(other.entries[key]);
+ }
+ }
+}
diff --git a/pub/code_transformers/lib/resolver.dart b/pub/code_transformers/lib/resolver.dart
new file mode 100644
index 0000000..faf34db
--- /dev/null
+++ b/pub/code_transformers/lib/resolver.dart
@@ -0,0 +1,11 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Tools for working with resolved ASTs from Barback transformers.
+library code_transformers.resolver;
+
+export 'src/dart_sdk.dart';
+export 'src/entry_point.dart';
+export 'src/resolver.dart';
+export 'src/resolvers.dart';
diff --git a/pub/code_transformers/lib/src/async_benchmark_base.dart b/pub/code_transformers/lib/src/async_benchmark_base.dart
new file mode 100644
index 0000000..423ff64
--- /dev/null
+++ b/pub/code_transformers/lib/src/async_benchmark_base.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library code_transformers.src.async_benchmark_base;
+
+import 'dart:async';
+
+/// An adaptation of [BenchmarkBase] from the `benchmark_harness` package that
+/// works for async benchmarks.
+/// TODO(jakemac): Get this merged into `benchmark_harness`.
+class AsyncBenchmarkBase {
+ // Empty constructor.
+ const AsyncBenchmarkBase();
+
+ // The benchmark code.
+ // This function is not used, if both [warmup] and [exercise] are overwritten.
+ Future run() => new Future.value();
+
+ // Runs a short version of the benchmark. By default invokes [run] once.
+ Future warmup() => run();
+
+ // Exercices the benchmark. By default invokes [run] 10 times.
+ Future exercise({int iterations: 10}) {
+ var i = 0;
+ return Future.doWhile(() {
+ if (i >= iterations) return new Future.value(false);
+ i++;
+ return run().then((_) => true);
+ });
+ }
+
+ // Not measured setup code executed prior to the benchmark runs.
+ Future setup() => new Future.value();
+
+ // Not measures teardown code executed after the benchark runs.
+ Future teardown() => new Future.value();
+
+ // Measures the score for this benchmark by executing it repeatedly until
+ // time minimum has been reached.
+ static Future<double> measureFor(Function f, int minimumMillis) {
+ int minimumMicros = minimumMillis * 1000;
+ int iter = 0;
+ Stopwatch watch = new Stopwatch();
+ watch.start();
+ int elapsed = 0;
+ return Future.doWhile(() {
+ if (elapsed > minimumMicros) return new Future.value(false);
+ return f().then((_) {
+ elapsed = watch.elapsedMicroseconds;
+ iter++;
+ return true;
+ });
+ }).then((_) => elapsed / iter);
+ }
+
+ // Measures the average time to call `run` once and returns it.
+ Future<double> measure({int iterations: 10}) async {
+ // Unmeasured setup code.
+ await setup();
+ // Warmup for at least 100ms. Discard result.
+ await measureFor(() => warmup(), 100);
+ // Run the benchmark for at least 2000ms.
+ var result = await measureFor(() => exercise(iterations: iterations), 2000);
+ // Tear down the test (unmeasured) and return the result divided by the
+ // number of iterations.
+ await teardown();
+ return result / iterations;
+ }
+}
diff --git a/pub/code_transformers/lib/src/dart_sdk.dart b/pub/code_transformers/lib/src/dart_sdk.dart
new file mode 100644
index 0000000..2b56f86
--- /dev/null
+++ b/pub/code_transformers/lib/src/dart_sdk.dart
@@ -0,0 +1,271 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library code_transformers.src.dart_sdk;
+
+import 'dart:io' show Directory;
+
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:analyzer/src/context/context.dart';
+import 'package:analyzer/src/dart/sdk/sdk.dart';
+import 'package:analyzer/src/generated/engine.dart';
+import 'package:analyzer/src/generated/sdk.dart';
+import 'package:analyzer/src/generated/source.dart';
+import 'package:analyzer/src/summary/idl.dart';
+import 'package:cli_util/cli_util.dart' as cli_util;
+
+/// Attempts to provide the current Dart SDK directory.
+///
+/// This will return null if the SDK cannot be found
+///
+/// Note that this may not be correct when executing outside of `pub`.
+String get dartSdkDirectory {
+ Directory sdkDir = cli_util.getSdkDir();
+ return sdkDir != null ? sdkDir.path : null;
+}
+
+/// Sources that are annotated with a source uri, so it is easy to resolve how
+/// to support `Resolver.getImportUri`.
+abstract class UriAnnotatedSource extends Source {
+ Uri get uri;
+}
+
+/// Dart SDK which wraps all Dart sources as [UriAnnotatedSource] to ensure they
+/// are tracked with Uris.
+class FolderBasedDartSdkProxy extends FolderBasedDartSdk {
+ FolderBasedDartSdkProxy(
+ ResourceProvider resourceProvider, String sdkDirectory)
+ : super(resourceProvider, resourceProvider.getFolder(sdkDirectory));
+
+ Source mapDartUri(String dartUri) =>
+ DartSourceProxy.wrap(super.mapDartUri(dartUri), Uri.parse(dartUri));
+}
+
+/// Dart SDK resolver which wraps all Dart sources to ensure they are tracked
+/// with URIs.
+class DartUriResolverProxy implements DartUriResolver {
+ final DartUriResolver _proxy;
+ DartUriResolverProxy(DartSdk sdk) : _proxy = new DartUriResolver(sdk);
+
+ Source resolveAbsolute(Uri uri, [Uri actualUri]) =>
+ DartSourceProxy.wrap(_proxy.resolveAbsolute(uri, actualUri), uri);
+
+ DartSdk get dartSdk => _proxy.dartSdk;
+
+ Source fromEncoding(UriKind kind, Uri uri) =>
+ throw new UnsupportedError('fromEncoding is not supported');
+
+ Uri restoreAbsolute(Source source) =>
+ throw new UnsupportedError('restoreAbsolute is not supported');
+}
+
+/// Source file for dart: sources which track the sources with dart: URIs.
+///
+/// This is primarily to support [Resolver.getImportUri] for Dart SDK (dart:)
+/// based libraries.
+class DartSourceProxy implements UriAnnotatedSource {
+ /// Absolute URI which this source can be imported from
+ final Uri uri;
+
+ /// Underlying source object.
+ final Source _proxy;
+
+ Source get source => this;
+
+ DartSourceProxy(this._proxy, this.uri);
+
+ /// Ensures that [source] is a DartSourceProxy.
+ static DartSourceProxy wrap(Source source, Uri uri) {
+ if (source == null || source is DartSourceProxy) return source;
+ return new DartSourceProxy(source, uri);
+ }
+
+ // Note: to support both analyzer versions <0.22.0 and analyzer >=0.22.0, we
+ // implement both `resolveRelative` and `resolveRelativeUri`. Only one of them
+ // is available at a time in the analyzer package, so we use the `as dynamic`
+ // in these methods to hide warnings for the code that is missing. These APIs
+ // are invoked from the analyzer itself, so we don't expect them to cause
+ // failures.
+ Source resolveRelative(Uri relativeUri) {
+ // Assume that the type can be accessed via this URI, since these
+ // should only be parts for dart core files.
+ return wrap((_proxy as dynamic).resolveRelative(relativeUri), uri);
+ }
+
+ Uri resolveRelativeUri(Uri relativeUri) {
+ return (_proxy as dynamic).resolveRelativeUri(relativeUri);
+ }
+
+ bool exists() => _proxy.exists();
+
+ bool operator ==(Object other) =>
+ (other is DartSourceProxy && _proxy == other._proxy);
+
+ int get hashCode => _proxy.hashCode;
+
+ TimestampedData<String> get contents => _proxy.contents;
+
+ String get encoding => _proxy.encoding;
+
+ String get fullName => _proxy.fullName;
+
+ Source get librarySource => _proxy.librarySource;
+
+ int get modificationStamp => _proxy.modificationStamp;
+
+ String get shortName => _proxy.shortName;
+
+ UriKind get uriKind => _proxy.uriKind;
+
+ bool get isInSystemLibrary => _proxy.isInSystemLibrary;
+}
+
+/// Dart SDK which contains a mock implementation of the SDK libraries. May be
+/// used to speed up resultion when most of the core libraries is not needed.
+class MockDartSdk implements DartSdk {
+ final Map<Uri, _MockSdkSource> _sources = {};
+ final bool reportMissing;
+ final Map<String, SdkLibrary> _libs = {};
+ final String sdkVersion = '0';
+ List<String> get uris => _sources.keys.map((uri) => '$uri').toList();
+ final InternalAnalysisContext context;
+ DartUriResolver _resolver;
+ DartUriResolver get resolver => _resolver;
+
+ MockDartSdk(Map<String, String> sources, AnalysisOptions options,
+ {this.reportMissing})
+ : this.context = new SdkAnalysisContext(options) {
+ sources.forEach((uriString, contents) {
+ var uri = Uri.parse(uriString);
+ _sources[uri] = new _MockSdkSource(uri, contents);
+ _libs[uriString] = new SdkLibraryImpl(uri.path)
+ ..setDart2JsLibrary()
+ ..setVmLibrary();
+ });
+ _resolver = new DartUriResolver(this);
+ context.sourceFactory = new SourceFactory([_resolver]);
+ }
+
+ List<SdkLibrary> get sdkLibraries => _libs.values.toList();
+ SdkLibrary getSdkLibrary(String dartUri) => _libs[dartUri];
+ Source mapDartUri(String dartUri) => _getSource(Uri.parse(dartUri));
+
+ Source fromEncoding(UriKind kind, Uri uri) {
+ if (kind != UriKind.DART_URI) {
+ throw new UnsupportedError('expected dart: uri kind, got $kind.');
+ }
+ return _getSource(uri);
+ }
+
+ Source _getSource(Uri uri) {
+ var src = _sources[uri];
+ if (src == null) {
+ if (reportMissing) print('warning: missing mock for $uri.');
+ _sources[uri] =
+ src = new _MockSdkSource(uri, 'library dart.${uri.path};');
+ }
+ return src;
+ }
+
+ @override
+ Source fromFileUri(Uri uri) {
+ throw new UnsupportedError('MockDartSdk.fromFileUri');
+ }
+
+ @override
+ PackageBundle getLinkedBundle() => null;
+}
+
+class _MockSdkSource implements UriAnnotatedSource {
+ /// Absolute URI which this source can be imported from.
+ final Uri uri;
+ final String _contents;
+
+ Source get source => this;
+
+ Source get librarySource => null;
+
+ _MockSdkSource(this.uri, this._contents);
+
+ bool exists() => true;
+
+ int get hashCode => uri.hashCode;
+
+ final int modificationStamp = 1;
+
+ TimestampedData<String> get contents =>
+ new TimestampedData(modificationStamp, _contents);
+
+ String get encoding => "${uriKind.encoding}$uri";
+
+ String get fullName => shortName;
+
+ String get shortName => uri.path;
+
+ UriKind get uriKind => UriKind.DART_URI;
+
+ bool get isInSystemLibrary => true;
+
+ Source resolveRelative(Uri relativeUri) =>
+ throw new UnsupportedError('not expecting relative urls in dart: mocks');
+
+ Uri resolveRelativeUri(Uri relativeUri) =>
+ throw new UnsupportedError('not expecting relative urls in dart: mocks');
+
+ bool operator ==(Object other) => identical(this, other);
+}
+
+/// Sample mock SDK sources.
+final Map<String, String> mockSdkSources = {
+ // The list of types below is derived from types that are used internally by
+ // the resolver (see _initializeFrom in analyzer/src/generated/resolver.dart).
+ 'dart:core': '''
+ library dart.core;
+
+ void print(Object o) {}
+
+ class Object {
+ String toString(){}
+ }
+ class Function {}
+ class StackTrace {}
+ class Symbol {}
+ class Type {}
+
+ class String {}
+ class bool {}
+ class num {
+ num operator +(num other) {}
+ }
+ class int extends num {
+ int operator-() {}
+ }
+ class double extends num {}
+ class DateTime {}
+ class Null {}
+
+ class Deprecated {
+ final String expires;
+ const Deprecated(this.expires);
+ }
+ const Object deprecated = const Deprecated("next release");
+ class _Override { const _Override(); }
+ const Object override = const _Override();
+ class _Proxy { const _Proxy(); }
+ const Object proxy = const _Proxy();
+
+ class Iterable<E> {}
+ class List<E> implements Iterable<E> {}
+ class Map<K, V> {}
+ ''',
+ 'dart:async': '''
+ class Future<T> {
+ Future then(callback) {}
+ class Stream<T> {}
+ ''',
+ 'dart:html': '''
+ library dart.html;
+ class HtmlElement {}
+ ''',
+};
diff --git a/pub/code_transformers/lib/src/delete_file.dart b/pub/code_transformers/lib/src/delete_file.dart
new file mode 100644
index 0000000..8eba05e
--- /dev/null
+++ b/pub/code_transformers/lib/src/delete_file.dart
@@ -0,0 +1,22 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Transformer that deletes everything that it sees, but only in release mode.
+library code_transformers.src.delete_file;
+
+import 'package:barback/barback.dart';
+
+// Deletes all files supplied in release mode.
+class DeleteFile extends Transformer {
+ BarbackSettings settings;
+
+ DeleteFile.asPlugin(this.settings);
+
+ /// Only apply to files in release mode.
+ isPrimary(_) => settings.mode == BarbackMode.RELEASE;
+
+ apply(Transform transform) {
+ transform.consumePrimary();
+ }
+}
diff --git a/pub/code_transformers/lib/src/entry_point.dart b/pub/code_transformers/lib/src/entry_point.dart
new file mode 100644
index 0000000..b654409
--- /dev/null
+++ b/pub/code_transformers/lib/src/entry_point.dart
@@ -0,0 +1,55 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+import 'dart:async';
+
+import 'package:analyzer/analyzer.dart' as analyzer;
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:barback/barback.dart';
+
+/// Checks to see if the provided AssetId is a Dart file in a directory which
+/// may contain entry points.
+///
+/// Directories are considered entry points if they are Dart files located in
+/// web/, test/, benchmark/ or example/.
+bool isPossibleDartEntryId(AssetId id) {
+ if (id.extension != '.dart') return false;
+
+ return ['benchmark', 'example', 'test', 'web']
+ .any((dir) => id.path.startsWith("$dir/"));
+}
+
+/// Checks to see if the provided Asset is possibly a Dart entry point.
+///
+/// Assets are considered entry points if they pass [isPossibleDartEntryId] and
+/// have a main() method.
+///
+/// Because this only analyzes the primary asset this may return true for files
+/// which are not dart entries if the file does not have a main() but does have
+/// parts or exports.
+Future<bool> isPossibleDartEntry(Asset asset) {
+ if (!isPossibleDartEntryId(asset.id)) return new Future.value(false);
+
+ return asset.readAsString().then((contents) {
+ return _couldBeEntrypoint(
+ analyzer.parseCompilationUnit(contents, suppressErrors: true));
+ });
+}
+
+bool _couldBeEntrypoint(CompilationUnit compilationUnit) {
+ // Allow two or fewer arguments so that entrypoints intended for use with
+ // [spawnUri] get counted.
+ var hasMain = compilationUnit.declarations.any((node) =>
+ node is FunctionDeclaration &&
+ node.name.name == "main" &&
+ node.functionExpression.parameters.parameters.length <= 2);
+
+ if (hasMain) return true;
+
+ // If it has an export or a part, assume the worst- that the main could be
+ // in there.
+ // We avoid loading those since this can be run from isPrimaryAsset calls
+ // where we do not have access to other sources.
+ return compilationUnit.directives
+ .any((node) => node is ExportDirective || node is PartDirective);
+}
diff --git a/pub/code_transformers/lib/src/messages.dart b/pub/code_transformers/lib/src/messages.dart
new file mode 100644
index 0000000..c91fe3d
--- /dev/null
+++ b/pub/code_transformers/lib/src/messages.dart
@@ -0,0 +1,73 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains all warning messages produced by the code_transformers package.
+library code_transformers.src.messages;
+
+import 'package:code_transformers/messages/messages.dart';
+
+const NO_ABSOLUTE_PATHS = const MessageTemplate(
+ const MessageId('code_transformers', 1),
+ 'absolute paths not allowed: "%-url-%"',
+ 'Absolute paths not allowed',
+ '''
+The transformers processing your code were trying to resolve a URL and identify
+a file that they correspond to. Currently only relative paths can be resolved.
+''');
+
+const INVALID_URL_TO_OTHER_PACKAGE = const MessageTemplate(
+ const MessageId('code_transformers', 2),
+ 'Invalid URL to reach to another package: %-url-%. Path '
+ 'reaching to other packages must first reach up all the '
+ 'way to the %-prefix-% directory. For example, try changing the URL '
+ 'to: %-fixedUrl-%',
+ 'Invalid URL to reach another package',
+ '''
+To reach an asset that belongs to another package, use `package:` URLs in
+Dart code, but in any other language (like HTML or CSS) use relative URLs that
+first go all the way to the `packages/` directory.
+
+The rules for correctly writing these imports are subtle and have a lot of
+special cases. Please review
+<https://www.dartlang.org/polymer/app-directories.html> to learn
+more.
+''');
+
+const INVALID_PREFIX_PATH = const MessageTemplate(
+ const MessageId('code_transformers', 3),
+ 'incomplete %-prefix-%/ path. It should have at least 3 '
+ 'segments %-prefix-%/name/path_from_name\'s_%-folder-%_dir',
+ 'Incomplete URL to asset in another package',
+ '''
+URLs that refer to assets in other packages need to explicitly mention the
+`packages/` directory. In the future this requirement might be removed, but for
+now you must use a canonical URL form for it.
+
+For example, if `packages/a/a.html` needs to import `packages/b/b.html`,
+you might expect a.html to import `../b/b.html`. Instead, it must import
+`../../packages/b/b.html`.
+
+See [issue 15797](http://dartbug.com/15797) and
+<https://www.dartlang.org/polymer/app-directories.html> to learn more.
+''');
+
+const UNSPECIFIED_FROM_IN_NON_LIB_ASSET = const MessageTemplate(
+ const MessageId('code_transformers', 4),
+ 'Cannot create URI for %-id-% without specifying where to import it from.',
+ 'Missing `from` argument.',
+ '''
+Assets outside of the lib folder can only be imported via relative URIs. Use
+the `from` argument in `assetIdToUri` to specify the location in the same
+package where you intend to import this asset from.
+''');
+
+const IMPORT_FROM_DIFFERENT_PACKAGE = const MessageTemplate(
+ const MessageId('code_transformers', 5),
+ 'Not possible to import %-toId-% from %-fromId-%',
+ 'Cannot import asset.',
+ '''
+Assets outside of the lib folder can only be imported via relative URIs from
+assets in the same package. To import an asset from another package, you need to
+move it into the lib folder of your package.
+''');
diff --git a/pub/code_transformers/lib/src/remove_sourcemap_comment.dart b/pub/code_transformers/lib/src/remove_sourcemap_comment.dart
new file mode 100644
index 0000000..70f4b9c
--- /dev/null
+++ b/pub/code_transformers/lib/src/remove_sourcemap_comment.dart
@@ -0,0 +1,31 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Transformer that removes any sourcemap comments from javascript files.
+library code_transformers.src.remove_sourcemap_comment;
+
+import 'package:barback/barback.dart';
+
+/// Transformer that removes any sourcemap comments from javascript files.
+/// Comments should be on their own line in the form: //# sourceMappingURL=*.map
+class RemoveSourcemapComment extends Transformer {
+ BarbackSettings settings;
+
+ RemoveSourcemapComment.asPlugin(this.settings);
+
+ /// Only apply to files in release mode.
+ isPrimary(_) => settings.mode == BarbackMode.RELEASE;
+
+ apply(Transform transform) {
+ var id = transform.primaryInput.id;
+ return transform.readInputAsString(id).then((file) {
+ if (file.contains(_SOURCE_MAP_COMMENT)) {
+ transform.addOutput(
+ new Asset.fromString(id, file.replaceAll(_SOURCE_MAP_COMMENT, '')));
+ }
+ });
+ }
+}
+
+final RegExp _SOURCE_MAP_COMMENT = new RegExp(r'\n\s*\/\/# sourceMappingURL.*');
diff --git a/pub/code_transformers/lib/src/resolver.dart b/pub/code_transformers/lib/src/resolver.dart
new file mode 100644
index 0000000..5b8ef1c
--- /dev/null
+++ b/pub/code_transformers/lib/src/resolver.dart
@@ -0,0 +1,109 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library code_transformer.src.resolver;
+
+import 'dart:async';
+
+import 'package:analyzer/dart/ast/ast.dart' show Expression;
+import 'package:analyzer/src/generated/constant.dart' show EvaluationResult;
+import 'package:analyzer/dart/element/element.dart';
+import 'package:barback/barback.dart';
+import 'package:source_maps/refactor.dart';
+import 'package:source_span/source_span.dart';
+
+/// Class for working with a barback based resolved AST.
+abstract class Resolver {
+ /// Update the status of all the sources referenced by the entry points and
+ /// update the resolved library. If [entryPoints] is omitted, the primary
+ /// asset of [transform] is used as the only entry point.
+ ///
+ /// [release] must be called when done handling this Resolver to allow it
+ /// to be used by later phases.
+ ///
+ /// If [resolveAllLibraries] is [false], then transitive imports will not
+ /// be resolved. This will result in faster resolution, but you will need to
+ /// manually call something like
+ /// `libary.context.computeLibraryElement(library.definingCompilationUnit.source);`
+ /// for each [LibraryElement] that you want to ensure is fully resolved. The
+ /// default value is [true].
+ Future<Resolver> resolve(Transform transform,
+ [List<AssetId> entryPoints, bool resolveAllLibraries]);
+
+ /// Release this resolver so it can be updated by following transforms.
+ void release();
+
+ /// Gets the resolved Dart library for an asset, or null if the AST has not
+ /// been resolved.
+ ///
+ /// If the AST has not been resolved then this normally means that the
+ /// transformer hosting this needs to be in an earlier phase.
+ LibraryElement getLibrary(AssetId assetId);
+
+ /// Gets all libraries accessible from the entry point, recursively.
+ ///
+ /// This includes all Dart SDK libraries as well.
+ Iterable<LibraryElement> get libraries;
+
+ /// Finds the first library identified by [libraryName], or null if no
+ /// library can be found.
+ LibraryElement getLibraryByName(String libraryName);
+
+ /// Finds the first library identified by [libraryName], or null if no
+ /// library can be found.
+ ///
+ /// [uri] must be an absolute URI of the form
+ /// `[dart:|package:]path/file.dart`.
+ LibraryElement getLibraryByUri(Uri uri);
+
+ /// Resolves a fully-qualified type name (library_name.ClassName).
+ ///
+ /// This will resolve the first instance of [typeName], because of potential
+ /// library name conflicts the name is not guaranteed to be unique.
+ ClassElement getType(String typeName);
+
+ /// Resolves a fully-qualified top-level library variable
+ /// (library_name.variableName).
+ ///
+ /// This will resolve the first instance of [variableName], because of
+ /// potential library name conflicts the name is not guaranteed to be unique.
+ Element getLibraryVariable(String variableName);
+
+ /// Resolves a fully-qualified top-level library function
+ /// (library_name.functionName).
+ ///
+ /// This will resolve the first instance of [functionName], because of
+ /// potential library name conflicts the name is not guaranteed to be unique.
+ Element getLibraryFunction(String functionName);
+
+ /// Gets the result of evaluating the constant [expression] in the context of
+ /// a [library].
+ EvaluationResult evaluateConstant(
+ LibraryElement library, Expression expression);
+
+ /// Gets an URI appropriate for importing the specified library.
+ ///
+ /// Returns null if the library cannot be imported via an absolute URI or
+ /// from [from] (if provided).
+ Uri getImportUri(LibraryElement lib, {AssetId from});
+
+ /// Get the asset ID of the file containing the asset.
+ AssetId getSourceAssetId(Element element);
+
+ /// Get the source span where the specified element was defined or null if
+ /// the element came from the Dart SDK.
+ SourceSpan getSourceSpan(Element element);
+
+ /// Get a [SourceFile] with the contents of the file that defines [element],
+ /// or null if the element came from the Dart SDK.
+ SourceFile getSourceFile(Element element);
+
+ /// Creates a text edit transaction for the given element if it is able
+ /// to be edited, returns null otherwise.
+ ///
+ /// The transaction contains the entire text of the source file where the
+ /// element originated. If the element was from a library part then the
+ /// source file is the part file rather than the library.
+ TextEditTransaction createTextEditTransaction(Element element);
+}
diff --git a/pub/code_transformers/lib/src/resolver_impl.dart b/pub/code_transformers/lib/src/resolver_impl.dart
new file mode 100644
index 0000000..1b87e88
--- /dev/null
+++ b/pub/code_transformers/lib/src/resolver_impl.dart
@@ -0,0 +1,582 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library code_transformer.src.resolver_impl;
+
+import 'dart:async';
+import 'package:analyzer/analyzer.dart' show parseDirectives;
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/src/generated/constant.dart'
+ show ConstantEvaluator, EvaluationResult;
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/src/generated/engine.dart';
+import 'package:analyzer/src/generated/sdk.dart' show DartSdk;
+import 'package:analyzer/src/generated/source.dart';
+import 'package:barback/barback.dart';
+import 'package:code_transformers/assets.dart';
+import 'package:path/path.dart' as native_path;
+import 'package:source_maps/refactor.dart';
+import 'package:source_span/source_span.dart';
+
+import 'resolver.dart';
+import 'dart_sdk.dart' show UriAnnotatedSource;
+
+// We should always be using url paths here since it's always Dart/pub code.
+final path = native_path.url;
+
+/// Resolves and updates an AST based on Barback-based assets.
+///
+/// This also provides a handful of useful APIs for traversing and working
+/// with the resolved AST.
+class ResolverImpl implements Resolver {
+ /// Cache of all asset sources currently referenced.
+ final Map<AssetId, AssetBasedSource> sources;
+
+ final InternalAnalysisContext _context =
+ AnalysisEngine.instance.createAnalysisContext();
+
+ /// Transform for which this is currently updating, or null when not updating.
+ Transform _currentTransform;
+
+ /// The currently resolved entry libraries, or null if nothing is resolved.
+ List<LibraryElement> _entryLibraries;
+ Set<LibraryElement> _libraries;
+
+ /// Future indicating when this resolver is done in the current phase.
+ Future _lastPhaseComplete = new Future.value();
+
+ /// Completer for wrapping up the current phase.
+ Completer _currentPhaseComplete;
+
+ /// Whether or not we are using a shared sources cache.
+ final bool _usingSharedSources;
+
+ /// Creates a resolver with a given [sdk] implementation for resolving
+ /// `dart:*` imports.
+ ResolverImpl(DartSdk sdk, DartUriResolver dartUriResolver,
+ {AnalysisOptions options, Map<AssetId, AssetBasedSource> sources})
+ : _usingSharedSources = sources != null,
+ sources = sources ?? <AssetId, AssetBasedSource>{} {
+ if (options == null) {
+ options = new AnalysisOptionsImpl()
+ ..cacheSize = 256 // # of sources to cache ASTs for.
+ ..preserveComments = false
+ ..analyzeFunctionBodies = true;
+ }
+ _context.analysisOptions = options;
+ _context.sourceFactory =
+ new SourceFactory([dartUriResolver, new _AssetUriResolver(this)]);
+ }
+
+ LibraryElement getLibrary(AssetId assetId) {
+ var source = sources[assetId];
+ return source == null ? null : _context.computeLibraryElement(source);
+ }
+
+ Future<Resolver> resolve(Transform transform,
+ [List<AssetId> entryPoints, bool resolveAllLibraries]) {
+ // Can only have one resolve in progress at a time, so chain the current
+ // resolution to be after the last one.
+ var phaseComplete = new Completer();
+ var future = _lastPhaseComplete.whenComplete(() {
+ _currentPhaseComplete = phaseComplete;
+ return _performResolve(
+ transform,
+ entryPoints == null ? [transform.primaryInput.id] : entryPoints,
+ resolveAllLibraries);
+ }).then((_) => this);
+ // Advance the lastPhaseComplete to be done when this phase is all done.
+ _lastPhaseComplete = phaseComplete.future;
+ return future;
+ }
+
+ void release() {
+ if (_currentPhaseComplete == null) {
+ throw new StateError('Releasing without current lock.');
+ }
+ _currentPhaseComplete.complete(null);
+ _currentPhaseComplete = null;
+
+ // Clear out libraries since they should not be referenced after release.
+ _entryLibraries = null;
+ _libraries = null;
+ _currentTransform = null;
+ }
+
+ Future _performResolve(Transform transform, List<AssetId> entryPoints,
+ bool resolveAllLibraries) {
+ resolveAllLibraries ??= true;
+ if (_currentTransform != null) {
+ throw new StateError('Cannot be accessed by concurrent transforms');
+ }
+ _currentTransform = transform;
+
+ // Basic approach is to start at the first file, update it's contents
+ // and see if it changed, then walk all files accessed by it.
+ var visited = new Set<AssetId>();
+ var visiting = new FutureGroup();
+ var toUpdate = [];
+
+ void processAsset(AssetId assetId) {
+ visited.add(assetId);
+
+ visiting.add(transform.readInputAsString(assetId).then((contents) {
+ var source = sources[assetId];
+ if (source == null) {
+ source = new AssetBasedSource(assetId, this);
+ sources[assetId] = source;
+ }
+ source.updateDependencies(contents);
+ toUpdate.add(new _PendingUpdate(source, contents));
+ source.dependentAssets
+ .where((id) => !visited.contains(id))
+ .forEach(processAsset);
+ }, onError: (e) {
+ var source = sources[assetId];
+ if (source != null && source.exists()) {
+ _context.applyChanges(new ChangeSet()..removedSource(source));
+ sources[assetId].updateContents(null);
+ }
+ }));
+ }
+
+ entryPoints.forEach(processAsset);
+
+ // Once we have all asset sources updated with the new contents then
+ // resolve everything.
+ return visiting.future.then((_) {
+ var changeSet = new ChangeSet();
+ toUpdate.forEach((pending) => pending.apply(changeSet));
+
+ // If we aren't using shared sources, then remove from the cache any
+ // sources which are no longer reachable.
+ if (!_usingSharedSources) {
+ var unreachableAssets =
+ sources.keys.toSet().difference(visited).map((id) => sources[id]);
+ for (var unreachable in unreachableAssets) {
+ changeSet.removedSource(unreachable);
+ unreachable.updateContents(null);
+ sources.remove(unreachable.assetId);
+ }
+ }
+
+ // Update the analyzer context with the latest sources
+ _context.applyChanges(changeSet);
+ // Force resolve each entry point (the getter will ensure the library is
+ // computed first).
+ _entryLibraries = entryPoints.map((id) {
+ var source = sources[id];
+ if (source == null) return null;
+ return _context.computeLibraryElement(source);
+ }).toList();
+
+ if (resolveAllLibraries) {
+ // Force resolve all other available libraries. As of analyzer > 0.27.1
+ // this is necessary to get resolved constants.
+ var newLibraries = new Set<LibraryElement>();
+ for (var library in libraries) {
+ if (library.source.uri.scheme == 'dart' ||
+ _entryLibraries.contains(library)) {
+ newLibraries.add(library);
+ } else {
+ newLibraries.add(_context
+ .computeLibraryElement(library.definingCompilationUnit.source));
+ }
+ }
+ _libraries = newLibraries;
+ }
+ });
+ }
+
+ Iterable<LibraryElement> get libraries {
+ if (_libraries == null) {
+ // Note: we don't use `lib.visibleLibraries` because that excludes the
+ // exports seen in the entry libraries.
+ _libraries = new Set<LibraryElement>();
+ _entryLibraries.forEach(_collectLibraries);
+ }
+ return _libraries;
+ }
+
+ void _collectLibraries(LibraryElement lib) {
+ if (lib == null || _libraries.contains(lib)) return;
+ _libraries.add(lib);
+ lib.importedLibraries.forEach(_collectLibraries);
+ lib.exportedLibraries.forEach(_collectLibraries);
+ }
+
+ LibraryElement getLibraryByName(String libraryName) =>
+ libraries.firstWhere((l) => l.name == libraryName, orElse: () => null);
+
+ LibraryElement getLibraryByUri(Uri uri) =>
+ libraries.firstWhere((l) => getImportUri(l) == uri, orElse: () => null);
+
+ ClassElement getType(String typeName) {
+ var dotIndex = typeName.lastIndexOf('.');
+ var libraryName = dotIndex == -1 ? '' : typeName.substring(0, dotIndex);
+
+ var className =
+ dotIndex == -1 ? typeName : typeName.substring(dotIndex + 1);
+
+ for (var lib in libraries.where((l) => l.name == libraryName)) {
+ var type = lib.getType(className);
+ if (type != null) return type;
+ }
+ return null;
+ }
+
+ Element getLibraryVariable(String variableName) {
+ var dotIndex = variableName.lastIndexOf('.');
+ var libraryName = dotIndex == -1 ? '' : variableName.substring(0, dotIndex);
+
+ var name =
+ dotIndex == -1 ? variableName : variableName.substring(dotIndex + 1);
+
+ return libraries
+ .where((lib) => lib.name == libraryName)
+ .expand((lib) => lib.units)
+ .expand((unit) => unit.topLevelVariables)
+ .firstWhere((variable) => variable.name == name, orElse: () => null);
+ }
+
+ Element getLibraryFunction(String fnName) {
+ var dotIndex = fnName.lastIndexOf('.');
+ var libraryName = dotIndex == -1 ? '' : fnName.substring(0, dotIndex);
+
+ var name = dotIndex == -1 ? fnName : fnName.substring(dotIndex + 1);
+
+ return libraries
+ .where((lib) => lib.name == libraryName)
+ .expand((lib) => lib.units)
+ .expand((unit) => unit.functions)
+ .firstWhere((fn) => fn.name == name, orElse: () => null);
+ }
+
+ EvaluationResult evaluateConstant(
+ LibraryElement library, Expression expression) {
+ return new ConstantEvaluator(library.source, _context.typeProvider)
+ .evaluate(expression);
+ }
+
+ Uri getImportUri(LibraryElement lib, {AssetId from}) =>
+ _getSourceUri(lib, from: from);
+
+ /// Similar to getImportUri but will get the part URI for parts rather than
+ /// the library URI.
+ Uri _getSourceUri(Element element, {AssetId from}) {
+ var source = element.source;
+ if (source is AssetBasedSource) {
+ var uriString = assetIdToUri(source.assetId, from: from);
+ return uriString != null ? Uri.parse(uriString) : null;
+ } else if (source is UriAnnotatedSource) {
+ return source.uri;
+ }
+ // Should not be able to encounter any other source types.
+ throw new StateError('Unable to resolve URI for ${source.runtimeType}');
+ }
+
+ AssetId getSourceAssetId(Element element) {
+ var source = element.source;
+ if (source is AssetBasedSource) return source.assetId;
+ return null;
+ }
+
+ SourceSpan getSourceSpan(Element element) {
+ var sourceFile = getSourceFile(element);
+ if (sourceFile == null) return null;
+ return sourceFile.span(
+ element.computeNode().offset, element.computeNode().end);
+ }
+
+ TextEditTransaction createTextEditTransaction(Element element) {
+ if (element.source is! AssetBasedSource) return null;
+
+ // Cannot edit unless there is an active transformer.
+ if (_currentTransform == null) return null;
+
+ AssetBasedSource source = element.source;
+ // Cannot modify assets in other packages.
+ if (source.assetId.package != _currentTransform.primaryInput.id.package) {
+ return null;
+ }
+
+ var sourceFile = getSourceFile(element);
+ if (sourceFile == null) return null;
+
+ return new TextEditTransaction(source.rawContents, sourceFile);
+ }
+
+ /// Gets the SourceFile for the source of the element.
+ SourceFile getSourceFile(Element element) {
+ var assetId = getSourceAssetId(element);
+ if (assetId == null) return null;
+
+ var importUri = _getSourceUri(element);
+ var spanPath = importUri != null ? importUri.toString() : assetId.path;
+ return new SourceFile(sources[assetId].rawContents, url: spanPath);
+ }
+}
+
+/// Implementation of Analyzer's Source for Barback based assets.
+class AssetBasedSource extends Source {
+ /// Asset ID where this source can be found.
+ final AssetId assetId;
+
+ /// The resolver this is being used in.
+ final ResolverImpl _resolver;
+
+ /// Cache of dependent asset IDs, to avoid re-parsing the AST.
+ Iterable<AssetId> _dependentAssets;
+
+ /// The current revision of the file, incremented only when file changes.
+ int _revision = 0;
+
+ /// The file contents.
+ String _contents;
+
+ AssetBasedSource(this.assetId, this._resolver);
+
+ /// Update the dependencies of this source. This parses [contents] but avoids
+ /// any analyzer resolution.
+ void updateDependencies(String contents) {
+ if (contents == _contents) return;
+ var unit = parseDirectives(contents, suppressErrors: true);
+ _dependentAssets = unit.directives
+ .where((d) => d is UriBasedDirective)
+ .map((d) => _resolve(assetId, (d as UriBasedDirective).uri.stringValue,
+ _logger, _getSpan(d, contents)))
+ .where((id) => id != null)
+ .toSet();
+ }
+
+ /// Update the contents of this file with [contents].
+ ///
+ /// Returns true if the contents of this asset have changed.
+ bool updateContents(String contents) {
+ if (contents == _contents) return false;
+ _contents = contents;
+ ++_revision;
+ return true;
+ }
+
+ /// Contents of the file.
+ TimestampedData<String> get contents {
+ if (!exists()) throw new StateError('$assetId does not exist');
+
+ return new TimestampedData<String>(modificationStamp, _contents);
+ }
+
+ /// Contents of the file.
+ String get rawContents => _contents;
+
+ Uri get uri => Uri.parse('asset:${assetId.package}/${assetId.path}');
+
+ /// Logger for the current transform.
+ ///
+ /// Only valid while the resolver is updating assets.
+ TransformLogger get _logger => _resolver._currentTransform.logger;
+
+ /// Gets all imports/parts/exports which resolve to assets (non-Dart files).
+ Iterable<AssetId> get dependentAssets => _dependentAssets;
+
+ bool exists() => _contents != null;
+
+ bool operator ==(Object other) =>
+ other is AssetBasedSource && assetId == other.assetId;
+
+ int get hashCode => assetId.hashCode;
+
+ void getContentsToReceiver(Source_ContentReceiver receiver) {
+ receiver.accept(rawContents, modificationStamp);
+ }
+
+ String get encoding =>
+ "${uriKind.encoding}${assetId.package}/${assetId.path}";
+
+ String get fullName => assetId.toString();
+
+ int get modificationStamp => _revision;
+
+ String get shortName => path.basename(assetId.path);
+
+ UriKind get uriKind {
+ if (assetId.path.startsWith('lib/')) return UriKind.PACKAGE_URI;
+ return UriKind.FILE_URI;
+ }
+
+ bool get isInSystemLibrary => false;
+
+ Source resolveRelative(Uri relativeUri) {
+ var id = _resolve(assetId, relativeUri.toString(), _logger, null);
+ if (id == null) return null;
+
+ // The entire AST should have been parsed and loaded at this point.
+ var source = _resolver.sources[id];
+ if (source == null) {
+ _logger.error('Could not load asset $id');
+ }
+ return source;
+ }
+
+ Uri resolveRelativeUri(Uri relativeUri) {
+ var id = _resolve(assetId, relativeUri.toString(), _logger, null);
+ if (id == null) return uri.resolveUri(relativeUri);
+
+ // The entire AST should have been parsed and loaded at this point.
+ var source = _resolver.sources[id];
+ if (source == null) {
+ _logger.error('Could not load asset $id');
+ }
+ return source.uri;
+ }
+
+ /// For logging errors.
+ SourceSpan _getSpan(AstNode node, [String contents]) =>
+ _getSourceFile(contents).span(node.offset, node.end);
+
+ /// For logging errors.
+ SourceFile _getSourceFile([String contents]) {
+ var uri = assetIdToUri(assetId);
+ var path = uri != null ? uri : assetId.path;
+ return new SourceFile(contents != null ? contents : rawContents, url: path);
+ }
+}
+
+/// Implementation of Analyzer's UriResolver for Barback based assets.
+class _AssetUriResolver implements UriResolver {
+ final ResolverImpl _resolver;
+ _AssetUriResolver(this._resolver);
+
+ Source resolveAbsolute(Uri uri, [Uri actualUri]) {
+ assert(uri.scheme != 'dart');
+ var assetId;
+ if (uri.scheme == 'asset') {
+ var parts = path.split(uri.path);
+ assetId = new AssetId(parts[0], path.joinAll(parts.skip(1)));
+ } else {
+ assetId = _resolve(null, uri.toString(), logger, null);
+ if (assetId == null) {
+ logger.error('Unable to resolve asset ID for "$uri"');
+ return null;
+ }
+ }
+ var source = _resolver.sources[assetId];
+ // Analyzer expects that sources which are referenced but do not exist yet
+ // still exist, so just make an empty source.
+ if (source == null) {
+ source = new AssetBasedSource(assetId, _resolver);
+ _resolver.sources[assetId] = source;
+ }
+ return source;
+ }
+
+ Source fromEncoding(UriKind kind, Uri uri) =>
+ throw new UnsupportedError('fromEncoding is not supported');
+
+ Uri restoreAbsolute(Source source) =>
+ throw new UnsupportedError('restoreAbsolute is not supported');
+
+ TransformLogger get logger => _resolver._currentTransform.logger;
+}
+
+/// Get an asset ID for a URL relative to another source asset.
+AssetId _resolve(
+ AssetId source, String url, TransformLogger logger, SourceSpan span) {
+ if (url == null || url == '') return null;
+ var uri = Uri.parse(url);
+
+ // Workaround for dartbug.com/17156- pub transforms package: imports from
+ // files of the transformers package to have absolute /packages/ URIs.
+ if (uri.scheme == '' &&
+ path.isAbsolute(url) &&
+ uri.pathSegments[0] == 'packages') {
+ uri = Uri.parse('package:${uri.pathSegments.skip(1).join(path.separator)}');
+ }
+
+ if (uri.scheme == 'package') {
+ var segments = new List.from(uri.pathSegments);
+ var package = segments[0];
+ segments[0] = 'lib';
+ return new AssetId(package, segments.join(path.separator));
+ }
+ // Dart SDK libraries do not have assets.
+ if (uri.scheme == 'dart') return null;
+
+ return uriToAssetId(source, url, logger, span);
+}
+
+/// A completer that waits until all added [Future]s complete.
+// TODO(blois): Copied from quiver. Remove from here when it gets
+// added to dart:core. (See #6626.)
+class FutureGroup<E> {
+ static const _FINISHED = -1;
+
+ int _pending = 0;
+ Future _failedTask;
+ final Completer<List> _completer = new Completer<List>();
+ final List results = [];
+
+ /** Gets the task that failed, if any. */
+ Future get failedTask => _failedTask;
+
+ /**
+ * Wait for [task] to complete.
+ *
+ * If this group has already been marked as completed, a [StateError] will be
+ * thrown.
+ *
+ * If this group has a [failedTask], new tasks will be ignored, because the
+ * error has already been signaled.
+ */
+ void add(Future task) {
+ if (_failedTask != null) return;
+ if (_pending == _FINISHED) throw new StateError("Future already completed");
+
+ _pending++;
+ var i = results.length;
+ results.add(null);
+ task.then((res) {
+ results[i] = res;
+ if (_failedTask != null) return;
+ _pending--;
+ if (_pending == 0) {
+ _pending = _FINISHED;
+ _completer.complete(results);
+ }
+ }, onError: (e, s) {
+ if (_failedTask != null) return;
+ _failedTask = task;
+ _completer.completeError(e, s);
+ });
+ }
+
+ /**
+ * A Future that completes with a List of the values from all the added
+ * tasks, when they have all completed.
+ *
+ * If any task fails, this Future will receive the error. Only the first
+ * error will be sent to the Future.
+ */
+ Future<List<E>> get future => _completer.future;
+}
+
+/// A pending update to notify the resolver that a [Source] has been added or
+/// changed. This is used by the `_performResolve` algorithm above to apply all
+/// changes after it first discovers the transitive closure of files that are
+/// reachable from the sources.
+class _PendingUpdate {
+ AssetBasedSource source;
+ String content;
+
+ _PendingUpdate(this.source, this.content);
+
+ void apply(ChangeSet changeSet) {
+ if (!source.updateContents(content)) return;
+ if (source._revision == 1 && source._contents != null) {
+ changeSet.addedSource(source);
+ } else {
+ changeSet.changedSource(source);
+ }
+ }
+}
diff --git a/pub/code_transformers/lib/src/resolvers.dart b/pub/code_transformers/lib/src/resolvers.dart
new file mode 100644
index 0000000..b691902
--- /dev/null
+++ b/pub/code_transformers/lib/src/resolvers.dart
@@ -0,0 +1,159 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library code_transformers.src.resolvers;
+
+import 'dart:async';
+import 'package:barback/barback.dart';
+
+import 'package:analyzer/file_system/physical_file_system.dart';
+import 'package:analyzer/src/generated/engine.dart' show AnalysisOptions;
+import 'package:analyzer/src/generated/sdk.dart' show DartSdk;
+import 'package:analyzer/src/generated/engine.dart';
+import 'package:analyzer/src/generated/source.dart' show DartUriResolver;
+
+import 'entry_point.dart';
+import 'resolver.dart';
+import 'resolver_impl.dart';
+import 'dart_sdk.dart' hide dartSdkDirectory;
+
+/// Barback-based code resolvers which maintains up-to-date resolved ASTs for
+/// the specified code entry points.
+///
+/// This can used by transformers dependent on resolved ASTs to handle the
+/// resolution of the AST and cache the results between compilations.
+///
+/// If multiple transformers rely on a resolved AST they should (ideally) share
+/// the same Resolvers object to minimize re-parsing the AST.
+class Resolvers {
+ final Map<AssetId, Resolver> _resolvers = {};
+ final DartSdk dartSdk;
+ final DartUriResolver dartUriResolver;
+ final AnalysisOptions options;
+
+ /// Null unless `useSharedSources` is true. This option should only be used if
+ /// you know that files are always in a consistent state wherever this
+ /// resolvers object is used. Any time that [Resolvers#get] or
+ /// [Resolver#resolve] are called it will update the sources globally when
+ /// this option is in use.
+ final Map<AssetId, AssetBasedSource> sharedSources;
+
+ Resolvers.fromSdk(this.dartSdk, this.dartUriResolver,
+ {this.options, bool useSharedSources})
+ : sharedSources =
+ useSharedSources == true ? <AssetId, AssetBasedSource>{} : null;
+
+ factory Resolvers(String dartSdkDirectory,
+ {AnalysisOptions options, bool useSharedSources}) {
+ _initAnalysisEngine();
+ var sdk = new FolderBasedDartSdkProxy(
+ PhysicalResourceProvider.INSTANCE, dartSdkDirectory);
+ var uriResolver = new DartUriResolverProxy(sdk);
+ return new Resolvers.fromSdk(sdk, uriResolver,
+ options: options, useSharedSources: useSharedSources);
+ }
+
+ factory Resolvers.fromMock(Map<String, String> sources,
+ {bool reportMissing: false,
+ AnalysisOptions options,
+ bool useSharedSources}) {
+ _initAnalysisEngine();
+ var sdk = new MockDartSdk(sources, options, reportMissing: reportMissing);
+ return new Resolvers.fromSdk(sdk, sdk.resolver,
+ options: options, useSharedSources: useSharedSources);
+ }
+
+ /// Get a resolver for [transform]. If provided, this resolves the code
+ /// starting from each of the assets in [entryPoints]. If not, this resolves
+ /// the code starting from `transform.primaryInput.id` by default.
+ ///
+ /// [Resolver.release] must be called once it's done being used, or
+ /// [ResolverTransformer] should be used to automatically release the
+ /// resolver.
+ ///
+ /// See [Resolver#resolve] for more info on the `resolveAllLibraries` option.
+ Future<Resolver> get(Transform transform,
+ [List<AssetId> entryPoints, bool resolveAllLibraries]) {
+ var id = transform.primaryInput.id;
+ var resolver = _resolvers.putIfAbsent(
+ id,
+ () => new ResolverImpl(dartSdk, dartUriResolver,
+ options: options, sources: sharedSources));
+ return resolver.resolve(transform, entryPoints, resolveAllLibraries);
+ }
+}
+
+/// Transformer mixin which automatically gets and releases resolvers.
+///
+/// To use mix this class in, set the resolvers field and override
+/// [applyResolver].
+abstract class ResolverTransformer implements Transformer {
+ /// The cache of resolvers- must be set from subclass.
+ Resolvers resolvers;
+
+ /// See [Resolver#resolve] for more info - can be overridden by a subclass.
+ bool get resolveAllLibraries => true;
+
+ /// By default only process possible entry point assets.
+ ///
+ /// This is only a preliminary check based on the asset ID.
+ Future<bool> isPrimary(assetOrId) {
+ // assetOrId is to handle the transition from Asset to AssetID between
+ // pub 1.3 and 1.4. Once support for 1.3 is dropped this should only
+ // support AssetId.
+ var id = assetOrId is AssetId ? assetOrId : (assetOrId as Asset).id;
+ return new Future.value(isPossibleDartEntryId(id));
+ }
+
+ /// Check to see if this should apply with the resolver on the provided asset.
+ ///
+ /// By default this will only apply on possible Dart entry points (see
+ /// [isPossibleDartEntry]).
+ Future<bool> shouldApplyResolver(Asset asset) => isPossibleDartEntry(asset);
+
+ /// This provides a default implementation of `Transformer.apply` that will
+ /// get and release resolvers automatically. Internally this:
+ /// * Gets a resolver associated with the transform primary input.
+ /// * Does resolution to the code starting from that input.
+ /// * Calls [applyResolver].
+ /// * Then releases the resolver.
+ ///
+ /// Use [applyToEntryPoints] instead if you need to override the entry points
+ /// to run the resolver on.
+ Future apply(Transform transform) =>
+ shouldApplyResolver(transform.primaryInput).then((result) {
+ if (result) return applyToEntryPoints(transform);
+ });
+
+ /// Helper function to make it easy to write an `Transformer.apply` method
+ /// that automatically gets and releases the resolver. This is typically used
+ /// as follows:
+ ///
+ /// Future apply(Transform transform) {
+ /// var entryPoints = ...; // compute entry points
+ /// return applyToEntryPoints(transform, entryPoints);
+ /// }
+ Future applyToEntryPoints(Transform transform, [List<AssetId> entryPoints]) {
+ return resolvers
+ .get(transform, entryPoints, resolveAllLibraries)
+ .then((resolver) {
+ return new Future(() => applyResolver(transform, resolver))
+ .whenComplete(() {
+ resolver.release();
+ });
+ });
+ }
+
+ /// Invoked when the resolver is ready to be processed.
+ ///
+ /// Return a Future to indicate when apply is completed.
+ applyResolver(Transform transform, Resolver resolver);
+}
+
+bool _analysisEngineInitialized = false;
+_initAnalysisEngine() {
+ if (_analysisEngineInitialized) return;
+ _analysisEngineInitialized = true;
+ AnalysisEngine.instance.processRequiredPlugins();
+}
diff --git a/pub/code_transformers/pubspec.yaml b/pub/code_transformers/pubspec.yaml
new file mode 100644
index 0000000..2f43a52
--- /dev/null
+++ b/pub/code_transformers/pubspec.yaml
@@ -0,0 +1,17 @@
+name: code_transformers
+version: 0.4.2+4
+author: Dart Team <misc@dartlang.org>
+description: Collection of utilities related to creating barback transformers.
+homepage: https://github.com/dart-lang/code-transformers
+environment:
+ sdk: '>=1.0.0 <2.0.0'
+dependencies:
+ analyzer: '>=0.28.0 <0.30.0'
+ barback: '>=0.14.2 <0.16.0'
+ cli_util: '>=0.0.1 <0.1.0'
+ path: '>=0.9.0 <2.0.0'
+ source_maps: '>=0.9.4 <0.11.0'
+ source_span: '>=1.0.0 <2.0.0'
+dev_dependencies:
+ test: '>=0.12.1 <0.13.0'
+ transformer_test: '>=0.1.0 <0.3.0'
diff --git a/pub/code_transformers/tool/travis.sh b/pub/code_transformers/tool/travis.sh
new file mode 100755
index 0000000..37492cc
--- /dev/null
+++ b/pub/code_transformers/tool/travis.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+# Fail fast
+set -e
+
+# Analyze
+dartanalyzer --fatal-warnings lib/*dart lib/messages/*dart test/*dart
+
+# Test
+pub run test
+
+# Check format
+pub global activate dart_style
+pub global run dart_style:format --set-exit-if-changed --dry-run lib/ test/
diff --git a/pub/csslib/BUILD.gn b/pub/csslib/BUILD.gn
index 702f257..28830d5 100644
--- a/pub/csslib/BUILD.gn
+++ b/pub/csslib/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for csslib-0.13.4
+# This file is generated by importer.py for csslib-0.13.5
import("//build/dart/dart_package.gni")
diff --git a/pub/csslib/CHANGELOG.md b/pub/csslib/CHANGELOG.md
index 7c5222d..3d83b2b 100644
--- a/pub/csslib/CHANGELOG.md
+++ b/pub/csslib/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.13.5
+
+* Adds support for `@-moz-document`.
+* Adds support for `@supports`.
+
## 0.13.4
* Parses CSS 2.1 pseudo-elements as pseudo-elements instead of pseudo-classes.
diff --git a/pub/csslib/lib/parser.dart b/pub/csslib/lib/parser.dart
index 168c9da..4c65a29 100644
--- a/pub/csslib/lib/parser.dart
+++ b/pub/csslib/lib/parser.dart
@@ -23,6 +23,12 @@
part 'src/tokenizer.dart';
part 'src/tokenkind.dart';
+enum ClauseType {
+ none,
+ conjunction,
+ disjunction,
+}
+
/** Used for parser lookup ahead (used for nested selectors Less support). */
class ParserState extends TokenizerState {
final Token peekToken;
@@ -485,7 +491,12 @@
* mixin: '@mixin name [(args,...)] '{' declarations/ruleset '}'
* include: '@include name [(@arg,@arg1)]
* '@include name [(@arg...)]
- * content '@content'
+ * content: '@content'
+ * -moz-document: '@-moz-document' [ <url> | url-prefix(<string>) |
+ * domain(<string>) | regexp(<string) ]# '{'
+ * declarations
+ * '}'
+ * supports: '@supports' supports_condition group_rule_body
*/
processDirective() {
var start = _peekToken.span;
@@ -762,11 +773,14 @@
case TokenKind.DIRECTIVE_INCLUDE:
return processInclude(_makeSpan(start));
-
case TokenKind.DIRECTIVE_CONTENT:
// TODO(terry): TBD
_warning("@content not implemented.", _makeSpan(start));
return null;
+ case TokenKind.DIRECTIVE_MOZ_DOCUMENT:
+ return processDocumentDirective();
+ case TokenKind.DIRECTIVE_SUPPORTS:
+ return processSupportsDirective();
}
return null;
}
@@ -987,6 +1001,127 @@
return new IncludeDirective(name.name, params, span);
}
+ DocumentDirective processDocumentDirective() {
+ var start = _peekToken.span;
+ _next(); // '@-moz-document'
+ var functions = <LiteralTerm>[];
+ do {
+ var function;
+
+ // Consume function token: IDENT '('
+ var ident = identifier();
+ _eat(TokenKind.LPAREN);
+
+ // Consume function arguments.
+ if (ident.name == 'url-prefix' || ident.name == 'domain') {
+ // @-moz-document allows the 'url-prefix' and 'domain' functions to
+ // omit quotations around their argument, contrary to the standard
+ // in which they must be strings. To support this we consume a
+ // string with optional quotation marks, then reapply quotation
+ // marks so they're present in the emitted CSS.
+ var argumentStart = _peekToken.span;
+ var value = processQuotedString(true);
+ // Don't quote the argument if it's empty. '@-moz-document url-prefix()'
+ // is a common pattern used for browser detection.
+ var argument = value.isNotEmpty ? '"$value"' : '';
+ var argumentSpan = _makeSpan(argumentStart);
+
+ _eat(TokenKind.RPAREN);
+
+ var arguments = new Expressions(_makeSpan(argumentSpan))
+ ..add(new LiteralTerm(argument, argument, argumentSpan));
+ function = new FunctionTerm(
+ ident.name, ident.name, arguments, _makeSpan(ident.span));
+ } else {
+ function = processFunction(ident);
+ }
+
+ functions.add(function);
+ } while (_maybeEat(TokenKind.COMMA));
+
+ _eat(TokenKind.LBRACE);
+ var groupRuleBody = processGroupRuleBody();
+ _eat(TokenKind.RBRACE);
+ return new DocumentDirective(functions, groupRuleBody, _makeSpan(start));
+ }
+
+ SupportsDirective processSupportsDirective() {
+ var start = _peekToken.span;
+ _next(); // '@supports'
+ var condition = processSupportsCondition();
+ _eat(TokenKind.LBRACE);
+ var groupRuleBody = processGroupRuleBody();
+ _eat(TokenKind.RBRACE);
+ return new SupportsDirective(condition, groupRuleBody, _makeSpan(start));
+ }
+
+ SupportsCondition processSupportsCondition() {
+ if (_peekKind(TokenKind.IDENTIFIER)) {
+ return processSupportsNegation();
+ }
+
+ var start = _peekToken.span;
+ var conditions = <SupportsConditionInParens>[];
+ var clauseType = ClauseType.none;
+
+ while (true) {
+ conditions.add(processSupportsConditionInParens());
+
+ var type;
+ var text = _peekToken.text.toLowerCase();
+
+ if (text == 'and') {
+ type = ClauseType.conjunction;
+ } else if (text == 'or') {
+ type = ClauseType.disjunction;
+ } else {
+ break; // Done parsing clause.
+ }
+
+ if (clauseType == ClauseType.none) {
+ clauseType = type; // First operand and operator of clause.
+ } else if (clauseType != type) {
+ _error("Operators can't be mixed without a layer of parentheses",
+ _peekToken.span);
+ break;
+ }
+
+ _next(); // Consume operator.
+ }
+
+ if (clauseType == ClauseType.conjunction) {
+ return new SupportsConjunction(conditions, _makeSpan(start));
+ } else if (clauseType == ClauseType.disjunction) {
+ return new SupportsDisjunction(conditions, _makeSpan(start));
+ } else {
+ return conditions.first;
+ }
+ }
+
+ SupportsNegation processSupportsNegation() {
+ var start = _peekToken.span;
+ var text = _peekToken.text.toLowerCase();
+ if (text != 'not') return null;
+ _next(); // 'not'
+ var condition = processSupportsConditionInParens();
+ return new SupportsNegation(condition, _makeSpan(start));
+ }
+
+ SupportsConditionInParens processSupportsConditionInParens() {
+ var start = _peekToken.span;
+ _eat(TokenKind.LPAREN);
+ // Try to parse a condition.
+ var condition = processSupportsCondition();
+ if (condition != null) {
+ _eat(TokenKind.RPAREN);
+ return new SupportsConditionInParens.nested(condition, _makeSpan(start));
+ }
+ // Otherwise, parse a declaration.
+ var declaration = processDeclaration([]);
+ _eat(TokenKind.RPAREN);
+ return new SupportsConditionInParens(declaration, _makeSpan(start));
+ }
+
RuleSet processRuleSet([SelectorGroup selectorGroup]) {
if (selectorGroup == null) {
selectorGroup = processSelectorGroup();
@@ -998,6 +1133,24 @@
return null;
}
+ List<TreeNode> processGroupRuleBody() {
+ var nodes = <TreeNode>[];
+ while (!(_peekKind(TokenKind.RBRACE) || _peekKind(TokenKind.END_OF_FILE))) {
+ var directive = processDirective();
+ if (directive != null) {
+ nodes.add(directive);
+ continue;
+ }
+ var ruleSet = processRuleSet();
+ if (ruleSet != null) {
+ nodes.add(ruleSet);
+ continue;
+ }
+ break;
+ }
+ return nodes;
+ }
+
/**
* Look ahead to see if what should be a declaration is really a selector.
* If it's a selector than it's a nested selector. This support's Less'
diff --git a/pub/csslib/lib/src/css_printer.dart b/pub/csslib/lib/src/css_printer.dart
index d3b62d0..0304fd7 100644
--- a/pub/csslib/lib/src/css_printer.dart
+++ b/pub/csslib/lib/src/css_printer.dart
@@ -77,22 +77,73 @@
}
}
+ void visitDocumentDirective(DocumentDirective node) {
+ emit('$_newLine@-moz-document ');
+ node.functions.first.visit(this);
+ for (var function in node.functions.skip(1)) {
+ emit(',$_sp');
+ function.visit(this);
+ }
+ emit('$_sp{');
+ for (var ruleSet in node.groupRuleBody) {
+ ruleSet.visit(this);
+ }
+ emit('$_newLine}');
+ }
+
+ void visitSupportsDirective(SupportsDirective node) {
+ emit('$_newLine@supports ');
+ node.condition.visit(this);
+ emit('$_sp{');
+ for (var rule in node.groupRuleBody) {
+ rule.visit(this);
+ }
+ emit('$_newLine}');
+ }
+
+ void visitSupportsConditionInParens(SupportsConditionInParens node) {
+ emit('(');
+ node.condition.visit(this);
+ emit(')');
+ }
+
+ void visitSupportsNegation(SupportsNegation node) {
+ emit('not$_sp');
+ node.condition.visit(this);
+ }
+
+ void visitSupportsConjunction(SupportsConjunction node) {
+ node.conditions.first.visit(this);
+ for (var condition in node.conditions.skip(1)) {
+ emit('${_sp}and$_sp');
+ condition.visit(this);
+ }
+ }
+
+ void visitSupportsDisjunction(SupportsDisjunction node) {
+ node.conditions.first.visit(this);
+ for (var condition in node.conditions.skip(1)) {
+ emit('${_sp}or$_sp');
+ condition.visit(this);
+ }
+ }
+
void visitMediaDirective(MediaDirective node) {
- emit(' @media');
+ emit('$_newLine@media');
emitMediaQueries(node.mediaQueries);
- emit(' {');
+ emit('$_sp{');
for (var ruleset in node.rulesets) {
ruleset.visit(this);
}
- emit('$_newLine\}');
+ emit('$_newLine}');
}
void visitHostDirective(HostDirective node) {
- emit('\n@host {');
+ emit('$_newLine@host$_sp{');
for (var ruleset in node.rulesets) {
ruleset.visit(this);
}
- emit('$_newLine\}');
+ emit('$_newLine}');
}
/**
@@ -251,12 +302,11 @@
}
void visitDeclaration(Declaration node) {
- String importantAsString() => node.important ? '$_sp!important' : '';
-
- emit("${node.property}: ");
+ emit('${node.property}:$_sp');
node._expression.visit(this);
-
- emit("${importantAsString()}");
+ if (node.important) {
+ emit('$_sp!important');
+ }
}
void visitVarDefinition(VarDefinition node) {
diff --git a/pub/csslib/lib/src/tokenkind.dart b/pub/csslib/lib/src/tokenkind.dart
index 85e6d40..48696ab 100644
--- a/pub/csslib/lib/src/tokenkind.dart
+++ b/pub/csslib/lib/src/tokenkind.dart
@@ -161,6 +161,8 @@
static const int DIRECTIVE_INCLUDE = 655;
static const int DIRECTIVE_CONTENT = 656;
static const int DIRECTIVE_EXTEND = 657;
+ static const int DIRECTIVE_MOZ_DOCUMENT = 658;
+ static const int DIRECTIVE_SUPPORTS = 659;
// Media query operators
static const int MEDIA_OP_ONLY = 665; // Unary.
@@ -218,6 +220,8 @@
const {'type': TokenKind.DIRECTIVE_INCLUDE, 'value': 'include'},
const {'type': TokenKind.DIRECTIVE_CONTENT, 'value': 'content'},
const {'type': TokenKind.DIRECTIVE_EXTEND, 'value': 'extend'},
+ const {'type': TokenKind.DIRECTIVE_MOZ_DOCUMENT, 'value': '-moz-document'},
+ const {'type': TokenKind.DIRECTIVE_SUPPORTS, 'value': 'supports'},
];
static const List<Map<String, dynamic>> MEDIA_OPERATORS = const [
diff --git a/pub/csslib/lib/src/tree.dart b/pub/csslib/lib/src/tree.dart
index c3709d2..d6b9dbf 100644
--- a/pub/csslib/lib/src/tree.dart
+++ b/pub/csslib/lib/src/tree.dart
@@ -435,6 +435,111 @@
visit(VisitorBase visitor) => visitor.visitDirective(this);
}
+class DocumentDirective extends Directive {
+ final List<LiteralTerm> functions;
+ final List<TreeNode> groupRuleBody;
+
+ DocumentDirective(this.functions, this.groupRuleBody, SourceSpan span)
+ : super(span);
+
+ DocumentDirective clone() {
+ var clonedFunctions = <LiteralTerm>[];
+ for (var function in functions) {
+ clonedFunctions.add(function.clone());
+ }
+ var clonedGroupRuleBody = <TreeNode>[];
+ for (var rule in groupRuleBody) {
+ clonedGroupRuleBody.add(rule.clone());
+ }
+ return new DocumentDirective(clonedFunctions, clonedGroupRuleBody, span);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitDocumentDirective(this);
+}
+
+class SupportsDirective extends Directive {
+ final SupportsCondition condition;
+ final List<TreeNode> groupRuleBody;
+
+ SupportsDirective(this.condition, this.groupRuleBody, SourceSpan span)
+ : super(span);
+
+ SupportsDirective clone() {
+ var clonedCondition = condition.clone();
+ var clonedGroupRuleBody = <TreeNode>[];
+ for (var rule in groupRuleBody) {
+ clonedGroupRuleBody.add(rule.clone());
+ }
+ return new SupportsDirective(clonedCondition, clonedGroupRuleBody, span);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitSupportsDirective(this);
+}
+
+abstract class SupportsCondition extends TreeNode {
+ SupportsCondition(SourceSpan span) : super(span);
+}
+
+class SupportsConditionInParens extends SupportsCondition {
+ /// A [Declaration] or nested [SupportsCondition].
+ final TreeNode condition;
+
+ SupportsConditionInParens(Declaration declaration, SourceSpan span)
+ : condition = declaration,
+ super(span);
+
+ SupportsConditionInParens.nested(SupportsCondition condition, SourceSpan span)
+ : condition = condition,
+ super(span);
+
+ SupportsConditionInParens clone() =>
+ new SupportsConditionInParens(condition.clone(), span);
+
+ visit(VisitorBase visitor) => visitor.visitSupportsConditionInParens(this);
+}
+
+class SupportsNegation extends SupportsCondition {
+ final SupportsConditionInParens condition;
+
+ SupportsNegation(this.condition, SourceSpan span) : super(span);
+
+ SupportsNegation clone() => new SupportsNegation(condition.clone(), span);
+
+ visit(VisitorBase visitor) => visitor.visitSupportsNegation(this);
+}
+
+class SupportsConjunction extends SupportsCondition {
+ final List<SupportsConditionInParens> conditions;
+
+ SupportsConjunction(this.conditions, SourceSpan span) : super(span);
+
+ SupportsConjunction clone() {
+ var clonedConditions = <SupportsCondition>[];
+ for (var condition in conditions) {
+ clonedConditions.add(condition.clone());
+ }
+ return new SupportsConjunction(clonedConditions, span);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitSupportsConjunction(this);
+}
+
+class SupportsDisjunction extends SupportsCondition {
+ final List<SupportsConditionInParens> conditions;
+
+ SupportsDisjunction(this.conditions, SourceSpan span) : super(span);
+
+ SupportsDisjunction clone() {
+ var clonedConditions = <SupportsCondition>[];
+ for (var condition in conditions) {
+ clonedConditions.add(condition.clone());
+ }
+ return new SupportsDisjunction(clonedConditions, span);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitSupportsDisjunction(this);
+}
+
class ImportDirective extends Directive {
/** import name specified. */
final String import;
diff --git a/pub/csslib/lib/src/tree_printer.dart b/pub/csslib/lib/src/tree_printer.dart
index ac4512c..7ae67e4 100644
--- a/pub/csslib/lib/src/tree_printer.dart
+++ b/pub/csslib/lib/src/tree_printer.dart
@@ -90,6 +90,50 @@
output.depth--;
}
+ void visitDocumentDirective(DocumentDirective node) {
+ heading('DocumentDirective', node);
+ output.depth++;
+ output.writeNodeList('functions', node.functions);
+ output.writeNodeList('group rule body', node.groupRuleBody);
+ output.depth--;
+ }
+
+ void visitSupportsDirective(SupportsDirective node) {
+ heading('SupportsDirective', node);
+ output.depth++;
+ output.writeNode('condition', node.condition);
+ output.writeNodeList('group rule body', node.groupRuleBody);
+ output.depth--;
+ }
+
+ void visitSupportsConditionInParens(SupportsConditionInParens node) {
+ heading('SupportsConditionInParens', node);
+ output.depth++;
+ output.writeNode('condition', node.condition);
+ output.depth--;
+ }
+
+ void visitSupportsNegation(SupportsNegation node) {
+ heading('SupportsNegation', node);
+ output.depth++;
+ output.writeNode('condition', node.condition);
+ output.depth--;
+ }
+
+ void visitSupportsConjunction(SupportsConjunction node) {
+ heading('SupportsConjunction', node);
+ output.depth++;
+ output.writeNodeList('conditions', node.conditions);
+ output.depth--;
+ }
+
+ void visitSupportsDisjunction(SupportsDisjunction node) {
+ heading('SupportsDisjunction', node);
+ output.depth++;
+ output.writeNodeList('conditions', node.conditions);
+ output.depth--;
+ }
+
void visitPageDirective(PageDirective node) {
heading('PageDirective', node);
output.depth++;
diff --git a/pub/csslib/lib/visitor.dart b/pub/csslib/lib/visitor.dart
index 1123493..ad94033 100644
--- a/pub/csslib/lib/visitor.dart
+++ b/pub/csslib/lib/visitor.dart
@@ -20,6 +20,12 @@
visitNoOp(NoOp node);
visitTopLevelProduction(TopLevelProduction node);
visitDirective(Directive node);
+ visitDocumentDirective(DocumentDirective node);
+ visitSupportsDirective(SupportsDirective node);
+ visitSupportsConditionInParens(SupportsConditionInParens node);
+ visitSupportsNegation(SupportsNegation node);
+ visitSupportsConjunction(SupportsConjunction node);
+ visitSupportsDisjunction(SupportsDisjunction node);
visitMediaExpression(MediaExpression node);
visitMediaQuery(MediaQuery node);
visitMediaDirective(MediaDirective node);
@@ -152,6 +158,32 @@
}
}
+ visitDocumentDirective(DocumentDirective node) {
+ _visitNodeList(node.functions);
+ _visitNodeList(node.groupRuleBody);
+ }
+
+ visitSupportsDirective(SupportsDirective node) {
+ node.condition.visit(this);
+ _visitNodeList(node.groupRuleBody);
+ }
+
+ visitSupportsConditionInParens(SupportsConditionInParens node) {
+ node.condition.visit(this);
+ }
+
+ visitSupportsNegation(SupportsNegation node) {
+ node.condition.visit(this);
+ }
+
+ visitSupportsConjunction(SupportsConjunction node) {
+ _visitNodeList(node.conditions);
+ }
+
+ visitSupportsDisjunction(SupportsDisjunction node) {
+ _visitNodeList(node.conditions);
+ }
+
visitMediaDirective(MediaDirective node) {
for (var mediaQuery in node.mediaQueries) {
visitMediaQuery(mediaQuery);
diff --git a/pub/csslib/pubspec.yaml b/pub/csslib/pubspec.yaml
index 0e2e7da..81c97a8 100644
--- a/pub/csslib/pubspec.yaml
+++ b/pub/csslib/pubspec.yaml
@@ -1,5 +1,5 @@
name: csslib
-version: 0.13.4
+version: 0.13.5
author: Dart Team <misc@dartlang.org>
description: A library for parsing CSS.
homepage: https://github.com/dart-lang/csslib
diff --git a/pub/observable/.analysis_options b/pub/observable/.analysis_options
new file mode 100644
index 0000000..6f04432
--- /dev/null
+++ b/pub/observable/.analysis_options
@@ -0,0 +1,28 @@
+analyzer:
+ strong-mode: true
+linter:
+ rules:
+ # Errors
+ - avoid_empty_else
+ - control_flow_in_finally
+ - empty_statements
+ - test_types_in_equals
+ - throw_in_finally
+ - valid_regexps
+
+ # Style
+ - annotate_overrides
+ - avoid_init_to_null
+ - avoid_return_types_on_setters
+ - await_only_futures
+ - camel_case_types
+ - comment_references
+ - empty_catches
+ - empty_constructor_bodies
+ - hash_and_equals
+ - library_prefixes
+ - non_constant_identifier_names
+ - prefer_is_not_empty
+ - slash_for_doc_comments
+ - type_init_formals
+ - unrelated_type_equality_checks
diff --git a/pub/observable/.gitignore b/pub/observable/.gitignore
new file mode 100644
index 0000000..96bc6bb
--- /dev/null
+++ b/pub/observable/.gitignore
@@ -0,0 +1,8 @@
+# Files and directories created by pub
+.packages
+.pub/
+packages
+pubspec.lock
+
+# Directory created by dartdoc
+doc/api/
diff --git a/pub/observable/.travis.yml b/pub/observable/.travis.yml
new file mode 100644
index 0000000..4fb40c9
--- /dev/null
+++ b/pub/observable/.travis.yml
@@ -0,0 +1,8 @@
+language: dart
+
+dart:
+ - dev
+ # Currently requires a dev-branch in order to use properly.
+ # - stable
+
+script: ./tool/presubmit.sh
diff --git a/pub/observable/AUTHORS b/pub/observable/AUTHORS
new file mode 100644
index 0000000..0617765
--- /dev/null
+++ b/pub/observable/AUTHORS
@@ -0,0 +1,9 @@
+# Names should be added to this file with this pattern:
+#
+# For individuals:
+# Name <email address>
+#
+# For organizations:
+# Organization <fnmatch pattern>
+#
+Google Inc. <*@google.com>
diff --git a/pub/observable/BUILD.gn b/pub/observable/BUILD.gn
new file mode 100644
index 0000000..2c7626e
--- /dev/null
+++ b/pub/observable/BUILD.gn
@@ -0,0 +1,17 @@
+# This file is generated by importer.py for observable-0.17.0+1
+
+import("//build/dart/dart_package.gni")
+
+dart_package("observable") {
+ package_name = "observable"
+
+ source_dir = "lib"
+
+ disable_analysis = true
+
+ deps = [
+ "//third_party/dart-pkg/pub/meta",
+ "//third_party/dart-pkg/pub/quiver",
+ "//third_party/dart-pkg/pub/collection",
+ ]
+}
diff --git a/pub/observable/CHANGELOG.md b/pub/observable/CHANGELOG.md
new file mode 100644
index 0000000..d6f9d8f
--- /dev/null
+++ b/pub/observable/CHANGELOG.md
@@ -0,0 +1,67 @@
+## 0.17.0+1
+
+* Revert `PropertyChangeMixin`, which does not work in dart2js
+
+## 0.17.0
+
+This is a larger change with a goal of no runtime changes for current
+customers, but in the future `Observable` will [become][issue_10] a very
+lightweight interface, i.e.:
+
+```dart
+abstract class Observable<C extends ChangeRecord> {
+ Stream<List<C>> get changes;
+}
+```
+
+[issue_10]: https://github.com/dart-lang/observable/issues/10
+
+* Started deprecating the wide `Observable` interface
+ * `ChangeNotifier` should be used as a base class for these methods:
+ * `Observable.observed`
+ * `Observable.unobserved`
+ * `Observable.hasObservers`
+ * `Observable.deliverChanges`
+ * `Observable.notifyChange`
+ * `PropertyChangeNotifier` should be used for these methods:
+ * `Observable.notifyPropertyChange`
+ * Temporarily, `Observable` _uses_ `ChangeNotifier`
+ * Existing users of anything but `implements Observable` should
+ move to implementing or extending `ChangeNotifier`. In a
+ future release `Observable` will reduce API surface down to
+ an abstract `Stream<List<C>> get changes`.
+* Added the `ChangeNotifier` and `PropertyChangeNotifier` classes
+ * Can be used to implement `Observable` in a generic manner
+* Observable is now `Observable<C extends ChangeRecord>`
+ * When passing a generic type `C`, `notifyPropertyChange` is illegal
+
+## 0.16.0
+
+* Refactored `MapChangeRecord`
+ * Added equality and hashCode checks
+ * Added `MapChangeRecord.apply` to apply a change record
+* Added `MapDiffer`, which implements `Differ` for a `Map`
+
+## 0.15.0+2
+
+* Fix a bug in `ListDiffer` that caused a `RangeError`
+
+## 0.15.0+1
+
+* Fix analysis errors caused via missing `/*<E>*/` syntax in `0.15.0`
+
+## 0.15.0
+
+* Added the `Differ` interface, as well as `EqualityDiffer`
+* Refactored list diffing into a `ListDiffer`
+* Added concept of `ChangeRecord.ANY` and `ChangeRecord.NONE`
+ * Low-GC ways to expression "something/nothing" changed
+* Refactored `ListChangeRecord`
+ * Added named constructors for common use cases
+ * Added equality and hashCode checks
+ * Added `ListChangeRecord.apply` to apply a change record
+* Added missing `@override` annotations to satisfy `annotate_overrides`
+
+## 0.14.0+1
+
+* Add a missing dependency on `pkg/meta`.
diff --git a/pub/observable/CONTRIBUTING.md b/pub/observable/CONTRIBUTING.md
new file mode 100644
index 0000000..6f5e0ea
--- /dev/null
+++ b/pub/observable/CONTRIBUTING.md
@@ -0,0 +1,33 @@
+Want to contribute? Great! First, read this page (including the small print at
+the end).
+
+### Before you contribute
+Before we can use your code, you must sign the
+[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
+(CLA), which you can do online. The CLA is necessary mainly because you own the
+copyright to your changes, even after your contribution becomes part of our
+codebase, so we need your permission to use and distribute your code. We also
+need to be sure of various other things—for instance that you'll tell us if you
+know that your code infringes on other people's patents. You don't have to sign
+the CLA until after you've submitted your code for review and a member has
+approved it, but you must do it before we can put your code into our codebase.
+
+Before you start working on a larger contribution, you should get in touch with
+us first through the issue tracker with your idea so that we can help out and
+possibly guide you. Coordinating up front makes it much easier to avoid
+frustration later on.
+
+### Code reviews
+All submissions, including submissions by project members, require review.
+
+### File headers
+All files in the project must start with the following header.
+
+ // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
+ // for details. All rights reserved. Use of this source code is governed by a
+ // BSD-style license that can be found in the LICENSE file.
+
+### The small print
+Contributions made by corporations are covered by a different agreement than the
+one above, the
+[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate).
diff --git a/pub/observable/LICENSE b/pub/observable/LICENSE
new file mode 100644
index 0000000..82e9b52
--- /dev/null
+++ b/pub/observable/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2016, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pub/observable/PATENTS b/pub/observable/PATENTS
new file mode 100644
index 0000000..6954196
--- /dev/null
+++ b/pub/observable/PATENTS
@@ -0,0 +1,23 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Dart Project.
+
+Google hereby grants to you a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this
+section) patent license to make, have made, use, offer to sell, sell,
+import, transfer, and otherwise run, modify and propagate the contents
+of this implementation of Dart, where such license applies only to
+those patent claims, both currently owned by Google and acquired in
+the future, licensable by Google that are necessarily infringed by
+this implementation of Dart. This grant does not include claims that
+would be infringed only as a consequence of further modification of
+this implementation. If you or your agent or exclusive licensee
+institute or order or agree to the institution of patent litigation
+against any entity (including a cross-claim or counterclaim in a
+lawsuit) alleging that this implementation of Dart or any code
+incorporated within this implementation of Dart constitutes direct or
+contributory patent infringement, or inducement of patent
+infringement, then any patent rights granted to you under this License
+for this implementation of Dart shall terminate as of the date such
+litigation is filed.
diff --git a/pub/observable/README.md b/pub/observable/README.md
new file mode 100644
index 0000000..c688514
--- /dev/null
+++ b/pub/observable/README.md
@@ -0,0 +1,6 @@
+Support for detecting and being notified when an object is mutated.
+
+There are two general ways to detect changes:
+
+* Listen to `Observable.changes` and be notified when an object changes
+* Use `Differ.diff` to determine changes between two objects
diff --git a/pub/observable/lib/observable.dart b/pub/observable/lib/observable.dart
new file mode 100644
index 0000000..516ff70
--- /dev/null
+++ b/pub/observable/lib/observable.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observable;
+
+export 'src/change_notifier.dart' show ChangeNotifier, PropertyChangeNotifier;
+export 'src/differs.dart' show Differ, EqualityDiffer, ListDiffer, MapDiffer;
+export 'src/records.dart'
+ show ChangeRecord, ListChangeRecord, MapChangeRecord, PropertyChangeRecord;
+export 'src/observable.dart';
+export 'src/observable_list.dart';
+export 'src/observable_map.dart';
+export 'src/to_observable.dart';
diff --git a/pub/observable/lib/src/change_notifier.dart b/pub/observable/lib/src/change_notifier.dart
new file mode 100644
index 0000000..f60eb16
--- /dev/null
+++ b/pub/observable/lib/src/change_notifier.dart
@@ -0,0 +1,137 @@
+import 'dart:async';
+
+import 'package:meta/meta.dart';
+
+import 'internal.dart';
+import 'observable.dart';
+import 'records.dart';
+
+/// Supplies [changes] and various hooks to implement [Observable].
+///
+/// May use [notifyChange] to queue a change record; they are asynchronously
+/// delivered at the end of the VM turn.
+///
+/// [ChangeNotifier] may be extended, mixed in, or used as a delegate.
+class ChangeNotifier<C extends ChangeRecord> implements Observable<C> {
+ StreamController<List<C>> _changes;
+
+ bool _scheduled = false;
+ List<C> _queue;
+
+ /// Emits a list of changes when the state of the object changes.
+ ///
+ /// Changes should produced in order, if significant.
+ @override
+ Stream<List<C>> get changes {
+ return (_changes ??= new StreamController<List<C>>.broadcast(
+ sync: true,
+ onListen: observed,
+ onCancel: unobserved,
+ ))
+ .stream;
+ }
+
+ /// May override to be notified when [changes] is first observed.
+ @override
+ @protected
+ @mustCallSuper
+ void observed() {}
+
+ /// May override to be notified when [changes] is no longer observed.
+ @override
+ @protected
+ @mustCallSuper
+ void unobserved() {
+ _changes = _queue = null;
+ }
+
+ /// If [hasObservers], synchronously emits [changes] that have been queued.
+ ///
+ /// Returns `true` if changes were emitted.
+ @override
+ @protected
+ @mustCallSuper
+ bool deliverChanges() {
+ List<ChangeRecord> changes;
+ if (_scheduled && hasObservers) {
+ if (_queue != null) {
+ changes = freezeInDevMode(_queue);
+ _queue = null;
+ } else {
+ changes = ChangeRecord.ANY;
+ }
+ _scheduled = false;
+ _changes.add(changes);
+ }
+ return changes != null;
+ }
+
+ /// Whether [changes] has at least one active listener.
+ ///
+ /// May be used to optimize whether to produce change records.
+ @override
+ bool get hasObservers => _changes?.hasListener == true;
+
+ /// Schedules [change] to be delivered.
+ ///
+ /// If [change] is omitted then [ChangeRecord.ANY] will be sent.
+ ///
+ /// If there are no listeners to [changes], this method does nothing.
+ @override
+ void notifyChange([C change]) {
+ if (!hasObservers) {
+ return;
+ }
+ if (change != null) {
+ (_queue ??= <C>[]).add(change);
+ }
+ if (!_scheduled) {
+ scheduleMicrotask(deliverChanges);
+ _scheduled = true;
+ }
+ }
+
+ @Deprecated('Exists to make migrations off Observable easier')
+ @override
+ @protected
+ /*=T*/ notifyPropertyChange/*<T>*/(
+ Symbol field,
+ /*=T*/
+ oldValue,
+ /*=T*/
+ newValue,
+ ) {
+ throw new UnsupportedError('Not supported by ChangeNotifier');
+ }
+}
+
+/// Supplies property `changes` and various hooks to implement [Observable].
+///
+/// May use `notifyChange` or `notifyPropertyChange` to queue a property change
+/// record; they are asynchronously delivered at the end of the VM turn.
+///
+/// [PropertyChangeNotifier] may be extended or used as a delegate. To use as
+/// a mixin, instead use with [PropertyChangeMixin]:
+/// with ChangeNotifier<PropertyChangeRecord>, PropertyChangeMixin
+class PropertyChangeNotifier extends ChangeNotifier<PropertyChangeRecord> {
+ @override
+ /*=T*/ notifyPropertyChange/*<T>*/(
+ Symbol field,
+ /*=T*/
+ oldValue,
+ /*=T*/
+ newValue,
+ ) {
+ if (hasObservers && oldValue != newValue) {
+ notifyChange(
+ new PropertyChangeRecord/*<T>*/(
+ this,
+ field,
+ oldValue,
+ newValue,
+ ),
+ );
+ }
+ return newValue;
+ }
+}
diff --git a/pub/observable/lib/src/differs.dart b/pub/observable/lib/src/differs.dart
new file mode 100644
index 0000000..40f005e
--- /dev/null
+++ b/pub/observable/lib/src/differs.dart
@@ -0,0 +1,40 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observable.src.differs;
+
+import 'dart:math' as math;
+
+import 'package:collection/collection.dart';
+
+import 'records.dart';
+
+import 'internal.dart';
+
+part 'differs/list_differ.dart';
+part 'differs/map_differ.dart';
+
+/// Generic comparisons between two comparable objects.
+abstract class Differ<E> {
+ /// Returns a list of change records between [oldValue] and [newValue].
+ ///
+ /// A return value of an empty [ChangeRecord.NONE] means no changes found.
+ List<ChangeRecord> diff(E oldValue, E newValue);
+}
+
+/// Uses [Equality] to determine a simple [ChangeRecord.ANY] response.
+class EqualityDiffer<E> implements Differ<E> {
+ final Equality<E> _equality;
+
+ const EqualityDiffer([this._equality = const DefaultEquality()]);
+
+ const EqualityDiffer.identity() : this._equality = const IdentityEquality();
+
+ @override
+ List<ChangeRecord> diff(E oldValue, E newValue) {
+ return _equality.equals(oldValue, newValue)
+ ? ChangeRecord.NONE
+ : ChangeRecord.ANY;
+ }
+}
diff --git a/pub/observable/lib/src/differs/list_differ.dart b/pub/observable/lib/src/differs/list_differ.dart
new file mode 100644
index 0000000..58004a7
--- /dev/null
+++ b/pub/observable/lib/src/differs/list_differ.dart
@@ -0,0 +1,470 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of observable.src.differs;
+
+/// Determines differences between two lists, returning [ListChangeRecord]s.
+///
+/// While [ListChangeRecord] has more information and can be replayed they carry
+/// a more significant cost to calculate and create and should only be used when
+/// the details in the record will actually be used.
+///
+/// See also [EqualityDiffer] for a simpler comparison.
+class ListDiffer<E> implements Differ<List<E>> {
+ final Equality<E> _equality;
+
+ const ListDiffer([this._equality = const DefaultEquality()]);
+
+ @override
+ List<ListChangeRecord<E>> diff(List<E> e1, List<E> e2) {
+ return _calcSplices/*<E>*/(
+ e2,
+ _equality,
+ 0,
+ e2.length,
+ e1,
+ 0,
+ e1.length,
+ );
+ }
+}
+
+enum _Edit {
+ leave,
+ update,
+ add,
+ delete,
+}
+
+// Note: This function is *based* on the computation of the Levenshtein
+// "edit" distance. The one change is that "updates" are treated as two
+// edits - not one. With List splices, an update is really a delete
+// followed by an add. By retaining this, we optimize for "keeping" the
+// maximum array items in the original array. For example:
+//
+// 'xxxx123' -> '123yyyy'
+//
+// With 1-edit updates, the shortest path would be just to update all seven
+// characters. With 2-edit updates, we delete 4, leave 3, and add 4. This
+// leaves the substring '123' intact.
+List<List<int>> _calcEditDistance/*<E>*/(
+ List/*<E>*/ current,
+ int currentStart,
+ int currentEnd,
+ List/*<E>*/ old,
+ int oldStart,
+ int oldEnd,
+) {
+ // 'Deletion' columns.
+ final rowCount = oldEnd - oldStart + 1;
+ final columnCount = currentEnd - currentStart + 1;
+ final distances = new List<List<int>>(rowCount);
+
+ // 'Addition' rows. Initialize null column.
+ for (var i = 0; i < rowCount; i++) {
+ distances[i] = new List<int>(columnCount);
+ distances[i][0] = i;
+ }
+
+ // Initialize null row.
+ for (var j = 0; j < columnCount; j++) {
+ distances[0][j] = j;
+ }
+
+ for (var i = 1; i < rowCount; i++) {
+ for (var j = 1; j < columnCount; j++) {
+ if (old[oldStart + i - 1] == current[currentStart + j - 1]) {
+ distances[i][j] = distances[i - 1][j - 1];
+ } else {
+ final north = distances[i - 1][j] + 1;
+ final west = distances[i][j - 1] + 1;
+ distances[i][j] = math.min(north, west);
+ }
+ }
+ }
+
+ return distances;
+}
+
+// This starts at the final weight, and walks "backward" by finding
+// the minimum previous weight recursively until the origin of the weight
+// matrix.
+Iterable<_Edit> _spliceOperationsFromEditDistances(List<List<int>> distances) {
+ var i = distances.length - 1;
+ var j = distances[0].length - 1;
+ var current = distances[i][j];
+ final edits = <_Edit>[];
+ while (i > 0 || j > 0) {
+ if (i == 0) {
+ edits.add(_Edit.add);
+ j--;
+ continue;
+ }
+ if (j == 0) {
+ edits.add(_Edit.delete);
+ i--;
+ continue;
+ }
+ final northWest = distances[i - 1][j - 1];
+ final west = distances[i - 1][j];
+ final north = distances[i][j - 1];
+
+ final min = math.min(math.min(west, north), northWest);
+ if (min == northWest) {
+ if (northWest == current) {
+ edits.add(_Edit.leave);
+ } else {
+ edits.add(_Edit.update);
+ current = northWest;
+ }
+ i--;
+ j--;
+ } else if (min == west) {
+ edits.add(_Edit.delete);
+ i--;
+ current = west;
+ } else {
+ edits.add(_Edit.add);
+ j--;
+ current = north;
+ }
+ }
+
+ return edits.reversed;
+}
+
+int _sharedPrefix/*<E>*/(
+ Equality/*<E>*/ equality,
+ List/*<E>*/ e1,
+ List/*<E>*/ e2,
+ int searchLength,
+) {
+ for (var i = 0; i < searchLength; i++) {
+ if (!equality.equals(e1[i], e2[i])) {
+ return i;
+ }
+ }
+ return searchLength;
+}
+
+int _sharedSuffix/*<E>*/(
+ Equality/*<E>*/ equality,
+ List/*<E>*/ e1,
+ List/*<E>*/ e2,
+ int searchLength,
+) {
+ var index1 = e1.length;
+ var index2 = e2.length;
+ var count = 0;
+ while (count < searchLength && equality.equals(e1[--index1], e2[--index2])) {
+ count++;
+ }
+ return count;
+}
+
+// Lacking individual splice mutation information, the minimal set of
+// splices can be synthesized given the previous state and final state of an
+// array. The basic approach is to calculate the edit distance matrix and
+// choose the shortest path through it.
+//
+// Complexity: O(l * p)
+// l: The length of the current array
+// p: The length of the old array
+List<ListChangeRecord/*<E>*/ > _calcSplices/*<E>*/(
+ List/*<E>*/ current,
+ Equality/*<E>*/ equality,
+ int currentStart,
+ int currentEnd,
+ List/*<E>*/ old,
+ int oldStart,
+ int oldEnd,
+) {
+ var prefixCount = 0;
+ var suffixCount = 0;
+ final minLength = math.min(currentEnd - currentStart, oldEnd - oldStart);
+ if (currentStart == 0 && oldStart == 0) {
+ prefixCount = _sharedPrefix(
+ equality,
+ current,
+ old,
+ minLength,
+ );
+ }
+ if (currentEnd == current.length && oldEnd == old.length) {
+ suffixCount = _sharedSuffix(
+ equality,
+ current,
+ old,
+ minLength - prefixCount,
+ );
+ }
+
+ currentStart += prefixCount;
+ oldStart += prefixCount;
+ currentEnd -= suffixCount;
+ oldEnd -= suffixCount;
+
+ if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) {
+ return ChangeRecord.NONE;
+ }
+
+ if (currentStart == currentEnd) {
+ final spliceRemoved = old.sublist(oldStart, oldEnd);
+ return [
+ new ListChangeRecord/*<E>*/ .remove(
+ current,
+ currentStart,
+ spliceRemoved,
+ ),
+ ];
+ }
+ if (oldStart == oldEnd) {
+ return [
+ new ListChangeRecord/*<E>*/ .add(
+ current,
+ currentStart,
+ currentEnd - currentStart,
+ ),
+ ];
+ }
+
+ final ops = _spliceOperationsFromEditDistances(
+ _calcEditDistance(
+ current,
+ currentStart,
+ currentEnd,
+ old,
+ oldStart,
+ oldEnd,
+ ),
+ );
+
+ var spliceIndex = -1;
+ var spliceRemovals = /*<E>*/ [];
+ var spliceAddedCount = 0;
+
+ bool hasSplice() => spliceIndex != -1;
+ void resetSplice() {
+ spliceIndex = -1;
+ spliceRemovals = /*<E>*/ [];
+ spliceAddedCount = 0;
+ }
+
+ var splices = <ListChangeRecord/*<E>*/ >[];
+
+ var index = currentStart;
+ var oldIndex = oldStart;
+ for (final op in ops) {
+ switch (op) {
+ case _Edit.leave:
+ if (hasSplice()) {
+ splices.add(new ListChangeRecord/*<E>*/(
+ current,
+ spliceIndex,
+ removed: spliceRemovals,
+ addedCount: spliceAddedCount,
+ ));
+ resetSplice();
+ }
+ index++;
+ oldIndex++;
+ break;
+ case _Edit.update:
+ if (!hasSplice()) {
+ spliceIndex = index;
+ }
+ spliceAddedCount++;
+ index++;
+ spliceRemovals.add(old[oldIndex]);
+ oldIndex++;
+ break;
+ case _Edit.add:
+ if (!hasSplice()) {
+ spliceIndex = index;
+ }
+ spliceAddedCount++;
+ index++;
+ break;
+ case _Edit.delete:
+ if (!hasSplice()) {
+ spliceIndex = index;
+ }
+ spliceRemovals.add(old[oldIndex]);
+ oldIndex++;
+ break;
+ }
+ }
+ if (hasSplice()) {
+ splices.add(new ListChangeRecord/*<E>*/(
+ current,
+ spliceIndex,
+ removed: spliceRemovals,
+ addedCount: spliceAddedCount,
+ ));
+ }
+ assert(() {
+ splices = new List<ListChangeRecord/*<E>*/ >.unmodifiable(splices);
+ return true;
+ });
+ return splices;
+}
+
+int _intersect(int start1, int end1, int start2, int end2) {
+ return math.min(end1, end2) - math.max(start1, start2);
+}
+
+void _mergeSplices/*<E>*/(
+ List<ListChangeRecord/*<E>*/ > splices,
+ ListChangeRecord/*<E>*/ record,
+) {
+ var spliceIndex = record.index;
+ var spliceRemoved = record.removed;
+ var spliceAdded = record.addedCount;
+
+ var inserted = false;
+ var insertionOffset = 0;
+
+ // I think the way this works is:
+ // - the loop finds where the merge should happen
+ // - it applies the merge in a particular splice
+ // - then continues and updates the subsequent splices with any offset diff.
+ for (var i = 0; i < splices.length; i++) {
+ var current = splices[i];
+ current = splices[i] = new ListChangeRecord/*<E>*/(
+ current.object,
+ current.index + insertionOffset,
+ removed: current.removed,
+ addedCount: current.addedCount,
+ );
+
+ if (inserted) continue;
+
+ var intersectCount = _intersect(
+ spliceIndex,
+ spliceIndex + spliceRemoved.length,
+ current.index,
+ current.index + current.addedCount,
+ );
+ if (intersectCount >= 0) {
+ // Merge the two splices.
+ splices.removeAt(i);
+ i--;
+
+ insertionOffset -= current.addedCount - current.removed.length;
+ spliceAdded += current.addedCount - intersectCount;
+
+ final deleteCount =
+ spliceRemoved.length + current.removed.length - intersectCount;
+ if (spliceAdded == 0 && deleteCount == 0) {
+ // Merged splice is a no-op, discard.
+ inserted = true;
+ } else {
+ final removed = current.removed.toList();
+ if (spliceIndex < current.index) {
+ // Some prefix of splice.removed is prepended to current.removed.
+ removed.insertAll(
+ 0,
+ spliceRemoved.getRange(0, current.index - spliceIndex),
+ );
+ }
+ if (spliceIndex + spliceRemoved.length >
+ current.index + current.addedCount) {
+ // Some suffix of splice.removed is appended to current.removed.
+ removed.addAll(spliceRemoved.getRange(
+ current.index + current.addedCount - spliceIndex,
+ spliceRemoved.length,
+ ));
+ }
+ spliceRemoved = removed;
+ if (current.index < spliceIndex) {
+ spliceIndex = current.index;
+ }
+ }
+ } else if (spliceIndex < current.index) {
+ // Insert splice here.
+ inserted = true;
+ splices.insert(
+ i,
+ new ListChangeRecord/*<E>*/(
+ record.object,
+ spliceIndex,
+ removed: spliceRemoved,
+ addedCount: spliceAdded,
+ ),
+ );
+ i++;
+ final offset = spliceAdded - spliceRemoved.length;
+ current = splices[i] = new ListChangeRecord/*<E>*/(
+ current.object,
+ current.index + offset,
+ removed: current.removed,
+ addedCount: current.addedCount,
+ );
+ insertionOffset += offset;
+ }
+ }
+ if (!inserted) {
+ splices.add(new ListChangeRecord/*<E>*/(
+ record.object,
+ spliceIndex,
+ removed: spliceRemoved,
+ addedCount: spliceAdded,
+ ));
+ }
+}
+
+List<ListChangeRecord/*<E>*/ > _createInitialSplices/*<E>*/(
+ List/*<E>*/ list,
+ List<ListChangeRecord/*<E>*/ > records,
+) {
+ final splices = <ListChangeRecord/*<E>*/ >[];
+ for (var i = 0; i < records.length; i++) {
+ _mergeSplices(splices, records[i]);
+ }
+ return splices;
+}
+
+// We need to summarize change records. Consumers of these records want to
+// apply the batch sequentially, and ensure that they can find inserted
+// items by looking at that position in the list. This property does not
+// hold in our record-as-you-go records. Consider:
+//
+// var model = toObservable(['a', 'b']);
+// model.removeAt(1);
+// model.insertAll(0, ['c', 'd', 'e']);
+// model.removeRange(1, 3);
+// model.insert(1, 'f');
+//
+// Here, we inserted some records and then removed some of them.
+// If someone processed these records naively, they would "play back" the
+// insert incorrectly, because those items will be shifted.
+List<ListChangeRecord/*<E>*/ > projectListSplices/*<E>*/(
+ List/*<E>*/ list,
+ List<ListChangeRecord/*<E>*/ > records, [
+ Equality/*<E>*/ equality = const DefaultEquality/*<E>*/(),
+]) {
+ if (records.length <= 1) return records;
+ final splices = <ListChangeRecord/*<E>*/ >[];
+ final initialSplices = _createInitialSplices(list, records);
+ for (final splice in initialSplices) {
+ if (splice.addedCount == 1 && splice.removed.length == 1) {
+ if (splice.removed[0] != list[splice.index]) {
+ splices.add(splice);
+ }
+ continue;
+ }
+ splices.addAll(
+ _calcSplices(
+ list,
+ equality,
+ splice.index,
+ splice.index + splice.addedCount,
+ splice.removed,
+ 0,
+ splice.removed.length,
+ ),
+ );
+ }
+ return splices;
+}
diff --git a/pub/observable/lib/src/differs/map_differ.dart b/pub/observable/lib/src/differs/map_differ.dart
new file mode 100644
index 0000000..c6041a4
--- /dev/null
+++ b/pub/observable/lib/src/differs/map_differ.dart
@@ -0,0 +1,38 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of observable.src.differs;
+
+/// Determines differences between two maps, returning [MapChangeRecord]s.
+///
+/// While [MapChangeRecord] has more information and can be replayed they carry
+/// a more significant cost to calculate and create and should only be used when
+/// the details in the record will actually be used.
+///
+/// See also [EqualityDiffer] for a simpler comparison.
+class MapDiffer<K, V> implements Differ<Map<K, V>> {
+ const MapDiffer();
+
+ @override
+ List<MapChangeRecord<K, V>> diff(Map<K, V> oldValue, Map<K, V> newValue) {
+ if (identical(oldValue, newValue)) {
+ return ChangeRecord.NONE;
+ }
+ final changes = <MapChangeRecord<K, V>>[];
+ oldValue.forEach((oldK, oldV) {
+ final newV = newValue[oldK];
+ if (newV == null && !newValue.containsKey(oldK)) {
+ changes.add(new MapChangeRecord<K, V>.remove(oldK, oldV));
+ } else if (newV != oldV) {
+ changes.add(new MapChangeRecord<K, V>(oldK, oldV, newV));
+ }
+ });
+ newValue.forEach((newK, newV) {
+ if (!oldValue.containsKey(newK)) {
+ changes.add(new MapChangeRecord<K, V>.insert(newK, newV));
+ }
+ });
+ return freezeInDevMode(changes);
+ }
+}
diff --git a/pub/observable/lib/src/internal.dart b/pub/observable/lib/src/internal.dart
new file mode 100644
index 0000000..d0ae2e1
--- /dev/null
+++ b/pub/observable/lib/src/internal.dart
@@ -0,0 +1,12 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+List/*<E>*/ freezeInDevMode/*<E>*/(List/*<E>*/ list) {
+ if (list == null) return const [];
+ assert(() {
+ list = new List/*<E>*/ .unmodifiable(list);
+ return true;
+ });
+ return list;
+}
diff --git a/pub/observable/lib/src/observable.dart b/pub/observable/lib/src/observable.dart
new file mode 100644
index 0000000..2d2c49e
--- /dev/null
+++ b/pub/observable/lib/src/observable.dart
@@ -0,0 +1,107 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observable.src.observable;
+
+import 'dart:async';
+
+import 'package:meta/meta.dart';
+
+import 'change_notifier.dart';
+import 'records.dart';
+
+/// Represents an object with observable state or properties.
+///
+/// The interface does not require any specific technique to implement
+/// observability. You may implement it in the following ways:
+/// - Extend or mixin [ChangeNotifier]
+/// - Implement the interface yourself and provide your own implementation
+abstract class Observable<C extends ChangeRecord> {
+ // To be removed when https://github.com/dart-lang/observable/issues/10
+ final ChangeNotifier<C> _delegate = new ChangeNotifier<C>();
+
+ // Whether Observable was not given a type.
+ final bool _isNotGeneric = C == dynamic;
+
+ /// Emits a list of changes when the state of the object changes.
+ ///
+ /// Changes should produced in order, if significant.
+ Stream<List<C>> get changes => _delegate.changes;
+
+ /// May override to be notified when [changes] is first observed.
+ @protected
+ @mustCallSuper
+ @Deprecated('Use ChangeNotifier instead to have this method available')
+ // REMOVE IGNORE when https://github.com/dart-lang/observable/issues/10
+ // ignore: invalid_use_of_protected_member
+ void observed() => _delegate.observed();
+
+ /// May override to be notified when [changes] is no longer observed.
+ @protected
+ @mustCallSuper
+ @Deprecated('Use ChangeNotifier instead to have this method available')
+ // REMOVE IGNORE when https://github.com/dart-lang/observable/issues/10
+ // ignore: invalid_use_of_protected_member
+ void unobserved() => _delegate.unobserved();
+
+ /// True if this object has any observers.
+ @Deprecated('Use ChangeNotifier instead to have this method available')
+ bool get hasObservers => _delegate.hasObservers;
+
+ /// If [hasObservers], synchronously emits [changes] that have been queued.
+ ///
+ /// Returns `true` if changes were emitted.
+ @Deprecated('Use ChangeNotifier instead to have this method available')
+ // REMOVE IGNORE when https://github.com/dart-lang/observable/issues/10
+ // ignore: invalid_use_of_protected_member
+ bool deliverChanges() => _delegate.deliverChanges();
+
+ /// Notify that the [field] name of this object has been changed.
+ ///
+ /// The [oldValue] and [newValue] are also recorded. If the two values are
+ /// equal, no change will be recorded.
+ ///
+ /// For convenience this returns [newValue].
+ ///
+ /// ## Deprecated
+ ///
+ /// All [Observable] objects will no longer be required to emit change records
+ /// when any property changes. For example, `ObservableList` will only emit
+ /// on `ObservableList.changes`, instead of on `ObservableList.listChanges`.
+ ///
+ /// If you are using a typed `implements/extends Observable<C>`, it is illegal
+ /// to call this method - will throw an [UnsupportedError] when called.
+ @Deprecated('Use PropertyChangeNotifier')
+ /*=T*/ notifyPropertyChange/*<T>*/(
+ Symbol field,
+ /*=T*/
+ oldValue,
+ /*=T*/
+ newValue,
+ ) {
+ if (hasObservers && oldValue != newValue) {
+ if (_isNotGeneric) {
+ notifyChange(
+ new PropertyChangeRecord(
+ this,
+ field,
+ oldValue,
+ newValue,
+ ) as C,
+ );
+ } else {
+ throw new UnsupportedError('Generic typed Observable does not support');
+ }
+ }
+ return newValue;
+ }
+
+ /// Schedules [change] to be delivered.
+ ///
+ /// If [change] is omitted then [ChangeRecord.ANY] will be sent.
+ ///
+ /// If there are no listeners to [changes], this method does nothing.
+ @Deprecated('Use ChangeNotifier instead to have this method available')
+ void notifyChange([C change]) => _delegate.notifyChange(change);
+}
diff --git a/pub/observable/lib/src/observable_list.dart b/pub/observable/lib/src/observable_list.dart
new file mode 100644
index 0000000..1618ce6
--- /dev/null
+++ b/pub/observable/lib/src/observable_list.dart
@@ -0,0 +1,333 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observable.src.observable_list;
+
+import 'dart:async';
+import 'dart:collection' show ListBase, UnmodifiableListView;
+
+import 'differs.dart';
+import 'records.dart';
+import 'observable.dart' show Observable;
+
+/// Represents an observable list of model values. If any items are added,
+/// removed, or replaced, then observers that are listening to [changes]
+/// will be notified.
+class ObservableList<E> extends ListBase<E> with Observable {
+ List<ListChangeRecord> _listRecords;
+
+ StreamController<List<ListChangeRecord>> _listChanges;
+
+ /// The inner [List<E>] with the actual storage.
+ final List<E> _list;
+
+ /// Creates an observable list of the given [length].
+ ///
+ /// If no [length] argument is supplied an extendable list of
+ /// length 0 is created.
+ ///
+ /// If a [length] argument is supplied, a fixed size list of that
+ /// length is created.
+ ObservableList([int length])
+ : _list = length != null ? new List<E>(length) : <E>[];
+
+ /// Creates an observable list of the given [length].
+ ///
+ /// This constructor exists to work around an issue in the VM whereby
+ /// classes that derive from [ObservableList] and mixin other classes
+ /// require a default generative constructor in the super class that
+ /// does not take optional arguments.
+ ObservableList.withLength(int length) : this(length);
+
+ /// Creates an observable list with the elements of [other]. The order in
+ /// the list will be the order provided by the iterator of [other].
+ factory ObservableList.from(Iterable<E> other) =>
+ new ObservableList<E>()..addAll(other);
+
+ /// The stream of summarized list changes, delivered asynchronously.
+ ///
+ /// Each list change record contains information about an individual mutation.
+ /// The records are projected so they can be applied sequentially. For
+ /// example, this set of mutations:
+ ///
+ /// var model = new ObservableList.from(['a', 'b']);
+ /// model.listChanges.listen((records) => records.forEach(print));
+ /// model.removeAt(1);
+ /// model.insertAll(0, ['c', 'd', 'e']);
+ /// model.removeRange(1, 3);
+ /// model.insert(1, 'f');
+ ///
+ /// The change records will be summarized so they can be "played back", using
+ /// the final list positions to figure out which item was added:
+ ///
+ /// #<ListChangeRecord index: 0, removed: [], addedCount: 2>
+ /// #<ListChangeRecord index: 3, removed: [b], addedCount: 0>
+ ///
+ /// [deliverChanges] can be called to force synchronous delivery.
+ Stream<List<ListChangeRecord>> get listChanges {
+ if (_listChanges == null) {
+ // TODO(jmesserly): split observed/unobserved notions?
+ _listChanges = new StreamController.broadcast(
+ sync: true,
+ onCancel: () {
+ _listChanges = null;
+ },
+ );
+ }
+ return _listChanges.stream;
+ }
+
+ bool get hasListObservers => _listChanges != null && _listChanges.hasListener;
+
+ @override
+ int get length => _list.length;
+
+ @override
+ set length(int value) {
+ int len = _list.length;
+ if (len == value) return;
+
+ // Produce notifications if needed
+ _notifyChangeLength(len, value);
+ if (hasListObservers) {
+ if (value < len) {
+ _notifyListChange(value, removed: _list.getRange(value, len).toList());
+ } else {
+ _notifyListChange(len, addedCount: value - len);
+ }
+ }
+
+ _list.length = value;
+ }
+
+ @override
+ E operator [](int index) => _list[index];
+
+ @override
+ void operator []=(int index, E value) {
+ E oldValue = _list[index];
+ if (hasListObservers && oldValue != value) {
+ _notifyListChange(index, addedCount: 1, removed: [oldValue]);
+ }
+ _list[index] = value;
+ }
+
+ // Forwarders so we can reflect on the properties.
+ @override
+ bool get isEmpty => super.isEmpty;
+
+ @override
+ bool get isNotEmpty => super.isNotEmpty;
+
+ // TODO(jmesserly): should we support first/last/single? They're kind of
+ // dangerous to use in a path because they throw exceptions. Also we'd need
+ // to produce property change notifications which seems to conflict with our
+ // existing list notifications.
+
+ // The following methods are here so that we can provide nice change events.
+ @override
+ void setAll(int index, Iterable<E> iterable) {
+ if (iterable is! List && iterable is! Set) {
+ iterable = iterable.toList();
+ }
+ int length = iterable.length;
+ if (hasListObservers && length > 0) {
+ _notifyListChange(index,
+ addedCount: length, removed: _list.sublist(index, length));
+ }
+ _list.setAll(index, iterable);
+ }
+
+ @override
+ void add(E value) {
+ int len = _list.length;
+ _notifyChangeLength(len, len + 1);
+ if (hasListObservers) {
+ _notifyListChange(len, addedCount: 1);
+ }
+
+ _list.add(value);
+ }
+
+ @override
+ void addAll(Iterable<E> iterable) {
+ int len = _list.length;
+ _list.addAll(iterable);
+
+ _notifyChangeLength(len, _list.length);
+
+ int added = _list.length - len;
+ if (hasListObservers && added > 0) {
+ _notifyListChange(len, addedCount: added);
+ }
+ }
+
+ @override
+ bool remove(Object element) {
+ for (int i = 0; i < this.length; i++) {
+ if (this[i] == element) {
+ removeRange(i, i + 1);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @override
+ void removeRange(int start, int end) {
+ _rangeCheck(start, end);
+ int rangeLength = end - start;
+ int len = _list.length;
+
+ _notifyChangeLength(len, len - rangeLength);
+ if (hasListObservers && rangeLength > 0) {
+ _notifyListChange(start, removed: _list.getRange(start, end).toList());
+ }
+
+ _list.removeRange(start, end);
+ }
+
+ @override
+ void insertAll(int index, Iterable<E> iterable) {
+ if (index < 0 || index > length) {
+ throw new RangeError.range(index, 0, length);
+ }
+ // TODO(floitsch): we can probably detect more cases.
+ if (iterable is! List && iterable is! Set) {
+ iterable = iterable.toList();
+ }
+ int insertionLength = iterable.length;
+ // There might be errors after the length change, in which case the list
+ // will end up being modified but the operation not complete. Unless we
+ // always go through a "toList" we can't really avoid that.
+ int len = _list.length;
+ _list.length += insertionLength;
+
+ _list.setRange(index + insertionLength, this.length, this, index);
+ _list.setAll(index, iterable);
+
+ _notifyChangeLength(len, _list.length);
+
+ if (hasListObservers && insertionLength > 0) {
+ _notifyListChange(index, addedCount: insertionLength);
+ }
+ }
+
+ @override
+ void insert(int index, E element) {
+ if (index < 0 || index > length) {
+ throw new RangeError.range(index, 0, length);
+ }
+ if (index == length) {
+ add(element);
+ return;
+ }
+ // We are modifying the length just below the is-check. Without the check
+ // Array.copy could throw an exception, leaving the list in a bad state
+ // (with a length that has been increased, but without a new element).
+ if (index is! int) throw new ArgumentError(index);
+ _list.length++;
+ _list.setRange(index + 1, length, this, index);
+
+ _notifyChangeLength(_list.length - 1, _list.length);
+ if (hasListObservers) {
+ _notifyListChange(index, addedCount: 1);
+ }
+ _list[index] = element;
+ }
+
+ @override
+ E removeAt(int index) {
+ E result = this[index];
+ removeRange(index, index + 1);
+ return result;
+ }
+
+ void _rangeCheck(int start, int end) {
+ if (start < 0 || start > this.length) {
+ throw new RangeError.range(start, 0, this.length);
+ }
+ if (end < start || end > this.length) {
+ throw new RangeError.range(end, start, this.length);
+ }
+ }
+
+ void _notifyListChange(
+ int index, {
+ List removed: const [],
+ int addedCount: 0,
+ }) {
+ if (!hasListObservers) return;
+ if (_listRecords == null) {
+ _listRecords = [];
+ scheduleMicrotask(deliverListChanges);
+ }
+ _listRecords.add(new ListChangeRecord(
+ this,
+ index,
+ removed: removed,
+ addedCount: addedCount,
+ ));
+ }
+
+ void _notifyChangeLength(int oldValue, int newValue) {
+ notifyPropertyChange(#length, oldValue, newValue);
+ notifyPropertyChange(#isEmpty, oldValue == 0, newValue == 0);
+ notifyPropertyChange(#isNotEmpty, oldValue != 0, newValue != 0);
+ }
+
+ void discardListChanges() {
+ // Leave _listRecords set so we don't schedule another delivery.
+ if (_listRecords != null) _listRecords = [];
+ }
+
+ bool deliverListChanges() {
+ if (_listRecords == null) return false;
+ List<ListChangeRecord> records =
+ projectListSplices/*<E>*/(this, _listRecords);
+ _listRecords = null;
+
+ if (hasListObservers && records.isNotEmpty) {
+ _listChanges.add(new UnmodifiableListView<ListChangeRecord>(records));
+ return true;
+ }
+ return false;
+ }
+
+ /// Calculates the changes to the list, if lacking individual splice mutation
+ /// information.
+ ///
+ /// This is not needed for change records produced by [ObservableList] itself,
+ /// but it can be used if the list instance was replaced by another list.
+ ///
+ /// The minimal set of splices can be synthesized given the previous state and
+ /// final state of a list. The basic approach is to calculate the edit
+ /// distance matrix and choose the shortest path through it.
+ ///
+ /// Complexity is `O(l * p)` where `l` is the length of the current list and
+ /// `p` is the length of the old list.
+ static List<ListChangeRecord/*<E>*/ > calculateChangeRecords/*<E>*/(
+ List/*<E>*/ oldValue,
+ List/*<E>*/ newValue,
+ ) {
+ return const ListDiffer/*<E>*/().diff(oldValue, newValue);
+ }
+
+ /// Updates the [previous] list using the [changeRecords]. For added items,
+ /// the [current] list is used to find the current value.
+ static void applyChangeRecords(List<Object> previous, List<Object> current,
+ List<ListChangeRecord> changeRecords) {
+ if (identical(previous, current)) {
+ throw new ArgumentError("can't use same list for previous and current");
+ }
+
+ for (ListChangeRecord change in changeRecords) {
+ int addEnd = change.index + change.addedCount;
+ int removeEnd = change.index + change.removed.length;
+
+ Iterable addedItems = current.getRange(change.index, addEnd);
+ previous.replaceRange(change.index, removeEnd, addedItems);
+ }
+ }
+}
diff --git a/pub/observable/lib/src/observable_map.dart b/pub/observable/lib/src/observable_map.dart
new file mode 100644
index 0000000..caebbda
--- /dev/null
+++ b/pub/observable/lib/src/observable_map.dart
@@ -0,0 +1,167 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observable.src.observable_map;
+
+import 'dart:collection';
+
+import 'observable.dart';
+import 'records.dart';
+import 'to_observable.dart';
+
+// TODO(jmesserly): this needs to be faster. We currently require multiple
+// lookups per key to get the old value.
+// TODO(jmesserly): this doesn't implement the precise interfaces like
+// LinkedHashMap, SplayTreeMap or HashMap. However it can use them for the
+// backing store.
+
+/// Represents an observable map of model values. If any items are added,
+/// removed, or replaced, then observers that are listening to [changes]
+/// will be notified.
+class ObservableMap<K, V> extends Observable implements Map<K, V> {
+ final Map<K, V> _map;
+
+ /// Creates an observable map.
+ ObservableMap() : _map = new HashMap<K, V>();
+
+ /// Creates a new observable map using a [LinkedHashMap].
+ ObservableMap.linked() : _map = new LinkedHashMap<K, V>();
+
+ /// Creates a new observable map using a [SplayTreeMap].
+ ObservableMap.sorted() : _map = new SplayTreeMap<K, V>();
+
+ /// Creates an observable map that contains all key value pairs of [other].
+ /// It will attempt to use the same backing map type if the other map is a
+ /// [LinkedHashMap], [SplayTreeMap], or [HashMap]. Otherwise it defaults to
+ /// [HashMap].
+ ///
+ /// Note this will perform a shallow conversion. If you want a deep conversion
+ /// you should use [toObservable].
+ factory ObservableMap.from(Map<K, V> other) {
+ return new ObservableMap<K, V>.createFromType(other)..addAll(other);
+ }
+
+ /// Like [ObservableMap.from], but creates an empty map.
+ factory ObservableMap.createFromType(Map<K, V> other) {
+ ObservableMap<K, V> result;
+ if (other is SplayTreeMap) {
+ result = new ObservableMap<K, V>.sorted();
+ } else if (other is LinkedHashMap) {
+ result = new ObservableMap<K, V>.linked();
+ } else {
+ result = new ObservableMap<K, V>();
+ }
+ return result;
+ }
+
+ /// Creates a new observable map wrapping [other].
+ ObservableMap.spy(Map<K, V> other) : _map = other;
+
+ @override
+ Iterable<K> get keys => _map.keys;
+
+ @override
+ Iterable<V> get values => _map.values;
+
+ @override
+ int get length => _map.length;
+
+ @override
+ bool get isEmpty => length == 0;
+
+ @override
+ bool get isNotEmpty => !isEmpty;
+
+ @override
+ bool containsValue(Object value) => _map.containsValue(value);
+
+ @override
+ bool containsKey(Object key) => _map.containsKey(key);
+
+ @override
+ V operator [](Object key) => _map[key];
+
+ @override
+ void operator []=(K key, V value) {
+ if (!hasObservers) {
+ _map[key] = value;
+ return;
+ }
+
+ int len = _map.length;
+ V oldValue = _map[key];
+
+ _map[key] = value;
+
+ if (len != _map.length) {
+ notifyPropertyChange(#length, len, _map.length);
+ notifyChange(new MapChangeRecord.insert(key, value));
+ _notifyKeysValuesChanged();
+ } else if (oldValue != value) {
+ notifyChange(new MapChangeRecord(key, oldValue, value));
+ _notifyValuesChanged();
+ }
+ }
+
+ @override
+ void addAll(Map<K, V> other) {
+ other.forEach((K key, V value) {
+ this[key] = value;
+ });
+ }
+
+ @override
+ V putIfAbsent(K key, V ifAbsent()) {
+ int len = _map.length;
+ V result = _map.putIfAbsent(key, ifAbsent);
+ if (hasObservers && len != _map.length) {
+ notifyPropertyChange(#length, len, _map.length);
+ notifyChange(new MapChangeRecord.insert(key, result));
+ _notifyKeysValuesChanged();
+ }
+ return result;
+ }
+
+ @override
+ V remove(Object key) {
+ int len = _map.length;
+ V result = _map.remove(key);
+ if (hasObservers && len != _map.length) {
+ notifyChange(new MapChangeRecord.remove(key, result));
+ notifyPropertyChange(#length, len, _map.length);
+ _notifyKeysValuesChanged();
+ }
+ return result;
+ }
+
+ @override
+ void clear() {
+ int len = _map.length;
+ if (hasObservers && len > 0) {
+ _map.forEach((key, value) {
+ notifyChange(new MapChangeRecord.remove(key, value));
+ });
+ notifyPropertyChange(#length, len, 0);
+ _notifyKeysValuesChanged();
+ }
+ _map.clear();
+ }
+
+ @override
+ void forEach(void f(K key, V value)) => _map.forEach(f);
+
+ @override
+ String toString() => Maps.mapToString(this);
+
+ // Note: we don't really have a reasonable old/new value to use here.
+ // But this should fix "keys" and "values" in templates with minimal overhead.
+ void _notifyKeysValuesChanged() {
+ notifyChange(new PropertyChangeRecord(this, #keys, null, null));
+ _notifyValuesChanged();
+ }
+
+ void _notifyValuesChanged() {
+ notifyChange(new PropertyChangeRecord(this, #values, null, null));
+ }
+}
diff --git a/pub/observable/lib/src/records.dart b/pub/observable/lib/src/records.dart
new file mode 100644
index 0000000..87d7d4f
--- /dev/null
+++ b/pub/observable/lib/src/records.dart
@@ -0,0 +1,28 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observable.src.records;
+
+import 'package:collection/collection.dart';
+import 'package:quiver/core.dart';
+
+import 'internal.dart';
+
+part 'records/list_change_record.dart';
+part 'records/map_change_record.dart';
+part 'records/property_change_record.dart';
+
+/// Result of a change to an observed object.
+class ChangeRecord {
+ /// Signifies a change occurred, but without details of the specific change.
+ ///
+ /// May be used to produce lower-GC-pressure records where more verbose change
+ /// records will not be used directly.
+ static const List<ChangeRecord> ANY = const [const ChangeRecord()];
+
+ /// Signifies no changes occurred.
+ static const List<ChangeRecord> NONE = const [];
+
+ const ChangeRecord();
+}
diff --git a/pub/observable/lib/src/records/list_change_record.dart b/pub/observable/lib/src/records/list_change_record.dart
new file mode 100644
index 0000000..59867b2
--- /dev/null
+++ b/pub/observable/lib/src/records/list_change_record.dart
@@ -0,0 +1,131 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of observable.src.records;
+
+/// A [ChangeRecord] that denotes adding or removing nodes at [index].
+///
+/// It should be assumed that elements are [removed] *before* being added.
+///
+/// A [List<ListChangeRecord>] can be "played back" against the [List] using
+/// the final list positions to figure out which item was added - this removes
+/// the need to incur costly GC on the most common operation (adding).
+class ListChangeRecord<E> implements ChangeRecord {
+ /// How many elements were added at [index] (after removing elements).
+ final int addedCount;
+
+ /// Index of where the change occurred.
+ final int index;
+
+ /// List that changed.
+ final List<E> object;
+
+ /// Elements that were removed starting at [index] (before adding elements).
+ final List<E> removed;
+
+ factory ListChangeRecord(
+ List<E> object,
+ int index, {
+ List<E> removed: const [],
+ int addedCount: 0,
+ }) {
+ return new ListChangeRecord._(object, index, removed, addedCount);
+ }
+
+ /// Records an `add` operation at `object[index]` of [addedCount] elements.
+ ListChangeRecord.add(this.object, this.index, this.addedCount)
+ : removed = const [] {
+ _assertValidState();
+ }
+
+ /// Records a `remove` operation at `object[index]` of [removed] elements.
+ ListChangeRecord.remove(this.object, this.index, List<E> removed)
+ : this.removed = freezeInDevMode/*<E>*/(removed),
+ this.addedCount = 0 {
+ _assertValidState();
+ }
+
+ /// Records a `replace` operation at `object[index]` of [removed] elements.
+ ///
+ /// If [addedCount] is not specified it defaults to `removed.length`.
+ ListChangeRecord.replace(this.object, this.index, List<E> removed,
+ [int addedCount])
+ : this.removed = freezeInDevMode/*<E>*/(removed),
+ this.addedCount = addedCount ?? removed.length {
+ _assertValidState();
+ }
+
+ ListChangeRecord._(
+ this.object,
+ this.index,
+ this.removed,
+ this.addedCount,
+ ) {
+ _assertValidState();
+ }
+
+ /// What elements were added to [object].
+ Iterable<E> get added {
+ return addedCount == 0 ? const [] : object.getRange(index, addedCount);
+ }
+
+ /// Apply this change record to [list].
+ void apply(List<E> list) {
+ list
+ ..removeRange(index, index + removed.length)
+ ..insertAll(index, object.getRange(index, index + addedCount));
+ }
+
+ void _assertValidState() {
+ assert(() {
+ if (object == null) {
+ throw new ArgumentError.notNull('object');
+ }
+ if (index == null) {
+ throw new ArgumentError.notNull('index');
+ }
+ if (removed == null) {
+ throw new ArgumentError.notNull('removed');
+ }
+ if (addedCount == null || addedCount < 0) {
+ throw new ArgumentError('Invalid `addedCount`: $addedCount');
+ }
+ return true;
+ });
+ }
+
+ /// Returns whether [reference] index was changed in this operation.
+ bool indexChanged(int reference) {
+ // If reference was before the change then it wasn't changed.
+ if (reference < index) return false;
+
+ // If this was a shift operation anything after index is changed.
+ if (addedCount != removed.length) return true;
+
+ // Otherwise anything in the update range was changed.
+ return reference < index + addedCount;
+ }
+
+ @override
+ bool operator ==(Object o) {
+ if (o is ListChangeRecord<E>) {
+ return identical(object, o.object) &&
+ index == o.index &&
+ addedCount == o.addedCount &&
+ const ListEquality().equals(removed, o.removed);
+ }
+ return false;
+ }
+
+ @override
+ int get hashCode {
+ return hash4(object, index, addedCount, const ListEquality().hash(removed));
+ }
+
+ @override
+ String toString() => ''
+ '#<$ListChangeRecord index: $index, '
+ 'removed: $removed, '
+ 'addedCount: $addedCount>';
+}
diff --git a/pub/observable/lib/src/records/map_change_record.dart b/pub/observable/lib/src/records/map_change_record.dart
new file mode 100644
index 0000000..cd37fc9
--- /dev/null
+++ b/pub/observable/lib/src/records/map_change_record.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of observable.src.records;
+
+/// A [ChangeRecord] that denotes adding, removing, or updating a map.
+class MapChangeRecord<K, V> implements ChangeRecord {
+ /// The map key that changed.
+ final K key;
+
+ /// The previous value associated with this key.
+ ///
+ /// Is always `null` if [isInsert].
+ final V oldValue;
+
+ /// The new value associated with this key.
+ ///
+ /// Is always `null` if [isRemove].
+ final V newValue;
+
+ /// True if this key was inserted.
+ final bool isInsert;
+
+ /// True if this key was removed.
+ final bool isRemove;
+
+ /// Create an update record of [key] from [oldValue] to [newValue].
+ const MapChangeRecord(this.key, this.oldValue, this.newValue)
+ : isInsert = false,
+ isRemove = false;
+
+ /// Create an insert record of [key] and [newValue].
+ const MapChangeRecord.insert(this.key, this.newValue)
+ : isInsert = true,
+ isRemove = false,
+ oldValue = null;
+
+ /// Create a remove record of [key] with a former [oldValue].
+ const MapChangeRecord.remove(this.key, this.oldValue)
+ : isInsert = false,
+ isRemove = true,
+ newValue = null;
+
+ /// Apply this change record to [map].
+ void apply(Map<K, V> map) {
+ if (isRemove) {
+ map.remove(key);
+ } else {
+ map[key] = newValue;
+ }
+ }
+
+ @override
+ bool operator ==(Object o) {
+ if (o is MapChangeRecord<K, V>) {
+ return key == o.key &&
+ oldValue == o.oldValue &&
+ newValue == o.newValue &&
+ isInsert == o.isInsert &&
+ isRemove == o.isRemove;
+ }
+ return false;
+ }
+
+ @override
+ int get hashCode {
+ return hashObjects([
+ key,
+ oldValue,
+ newValue,
+ isInsert,
+ isRemove,
+ ]);
+ }
+
+ @override
+ String toString() {
+ final kind = isInsert ? 'insert' : isRemove ? 'remove' : 'set';
+ return '#<MapChangeRecord $kind $key from $oldValue to $newValue';
+ }
+}
diff --git a/pub/observable/lib/src/records/property_change_record.dart b/pub/observable/lib/src/records/property_change_record.dart
new file mode 100644
index 0000000..74566d5
--- /dev/null
+++ b/pub/observable/lib/src/records/property_change_record.dart
@@ -0,0 +1,31 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of observable.src.records;
+
+/// A change record to a field of a generic observable object.
+class PropertyChangeRecord<T> implements ChangeRecord {
+ /// Object that changed.
+ final Object object;
+
+ /// Name of the property that changed.
+ final Symbol name;
+
+ /// Previous value of the property.
+ final T oldValue;
+
+ /// New value of the property.
+ final T newValue;
+
+ const PropertyChangeRecord(
+ this.object,
+ this.name,
+ this.oldValue,
+ this.newValue,
+ );
+
+ @override
+ String toString() => ''
+ '#<$PropertyChangeRecord $name from $oldValue to: $newValue';
+}
diff --git a/pub/observable/lib/src/to_observable.dart b/pub/observable/lib/src/to_observable.dart
new file mode 100644
index 0000000..3bf24b6
--- /dev/null
+++ b/pub/observable/lib/src/to_observable.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observable.src.to_observable;
+
+import 'dart:collection';
+
+import 'observable.dart' show Observable;
+import 'observable_list.dart' show ObservableList;
+import 'observable_map.dart' show ObservableMap;
+
+/// Converts the [Iterable] or [Map] to an [ObservableList] or [ObservableMap],
+/// respectively. This is a convenience function to make it easier to convert
+/// literals into the corresponding observable collection type.
+///
+/// If [value] is not one of those collection types, or is already [Observable],
+/// it will be returned unmodified.
+///
+/// If [value] is a [Map], the resulting value will use the appropriate kind of
+/// backing map: either [HashMap], [LinkedHashMap], or [SplayTreeMap].
+///
+/// By default this performs a deep conversion, but you can set [deep] to false
+/// for a shallow conversion. This does not handle circular data structures.
+/// If a conversion is peformed, mutations are only observed to the result of
+/// this function. Changing the original collection will not affect it.
+// TODO(jmesserly): ObservableSet?
+toObservable(dynamic value, {bool deep: true}) =>
+ deep ? _toObservableDeep(value) : _toObservableShallow(value);
+
+dynamic _toObservableShallow(dynamic value) {
+ if (value is Observable) return value;
+ if (value is Map) return new ObservableMap.from(value);
+ if (value is Iterable) return new ObservableList.from(value);
+ return value;
+}
+
+dynamic _toObservableDeep(dynamic value) {
+ if (value is Observable) return value;
+ if (value is Map) {
+ var result = new ObservableMap.createFromType(value);
+ value.forEach((k, v) {
+ result[_toObservableDeep(k)] = _toObservableDeep(v);
+ });
+ return result;
+ }
+ if (value is Iterable) {
+ return new ObservableList.from(value.map(_toObservableDeep));
+ }
+ return value;
+}
diff --git a/pub/observable/pubspec.yaml b/pub/observable/pubspec.yaml
new file mode 100644
index 0000000..f326c5d
--- /dev/null
+++ b/pub/observable/pubspec.yaml
@@ -0,0 +1,14 @@
+name: observable
+version: 0.17.0+1
+author: Dart Team <misc@dartlang.org>
+description: Support for marking objects as observable
+homepage: https://github.com/dart-lang/observable
+environment:
+ sdk: '>=1.19.0 <2.0.0'
+dependencies:
+ collection: '^1.11.0'
+ meta: '^1.0.4'
+ quiver: '^0.24.0'
+dev_dependencies:
+ dart_style: '^0.2.0'
+ test: '^0.12.0'
diff --git a/pub/observable/tool/presubmit.sh b/pub/observable/tool/presubmit.sh
new file mode 100755
index 0000000..364bee7
--- /dev/null
+++ b/pub/observable/tool/presubmit.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# Make sure dartfmt is run on everything
+# This assumes you have dart_style as a dev_dependency
+echo "Checking dartfmt..."
+NEEDS_DARTFMT="$(find lib test -name "*.dart" | xargs pub run dart_style:format -n)"
+if [[ ${NEEDS_DARTFMT} != "" ]]
+then
+ echo "FAILED"
+ echo "${NEEDS_DARTFMT}"
+ exit 1
+fi
+echo "PASSED"
+
+# Make sure we pass the analyzer
+echo "Checking dartanalyzer..."
+FAILS_ANALYZER="$(find lib test -name "*.dart" | xargs dartanalyzer --options .analysis_options)"
+if [[ $FAILS_ANALYZER == *"[error]"* ]]
+then
+ echo "FAILED"
+ echo "${FAILS_ANALYZER}"
+ exit 1
+fi
+echo "PASSED"
+
+# Fail on anything that fails going forward.
+set -e
+
+pub run test
diff --git a/pub/observe/.analysis_options b/pub/observe/.analysis_options
new file mode 100644
index 0000000..a10d4c5
--- /dev/null
+++ b/pub/observe/.analysis_options
@@ -0,0 +1,2 @@
+analyzer:
+ strong-mode: true
diff --git a/pub/observe/.gitignore b/pub/observe/.gitignore
new file mode 100644
index 0000000..f20a0d0
--- /dev/null
+++ b/pub/observe/.gitignore
@@ -0,0 +1,18 @@
+# Don’t commit the following directories created by pub.
+.pub
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.dart.precompiled.js
+*.js_
+*.js.deps
+*.js.map
+*.sw?
+.idea/
+.pub/
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/pub/observe/.test_config b/pub/observe/.test_config
new file mode 100644
index 0000000..dfc533f
--- /dev/null
+++ b/pub/observe/.test_config
@@ -0,0 +1,5 @@
+{
+ "test_package": {
+ "barback": true
+ }
+}
diff --git a/pub/observe/AUTHORS b/pub/observe/AUTHORS
new file mode 100644
index 0000000..0617765
--- /dev/null
+++ b/pub/observe/AUTHORS
@@ -0,0 +1,9 @@
+# Names should be added to this file with this pattern:
+#
+# For individuals:
+# Name <email address>
+#
+# For organizations:
+# Organization <fnmatch pattern>
+#
+Google Inc. <*@google.com>
diff --git a/pub/observe/BUILD.gn b/pub/observe/BUILD.gn
new file mode 100644
index 0000000..aa8261b
--- /dev/null
+++ b/pub/observe/BUILD.gn
@@ -0,0 +1,25 @@
+# This file is generated by importer.py for observe-0.15.0
+
+import("//build/dart/dart_package.gni")
+
+dart_package("observe") {
+ package_name = "observe"
+
+ source_dir = "lib"
+
+ disable_analysis = true
+
+ deps = [
+ "//third_party/dart-pkg/pub/source_maps",
+ "//third_party/dart-pkg/pub/barback",
+ "//third_party/dart-pkg/pub/code_transformers",
+ "//third_party/dart-pkg/pub/logging",
+ "//dart/third_party/pkg/func",
+ "//third_party/dart-pkg/pub/observable",
+ "//dart/pkg/analyzer",
+ "//third_party/dart-pkg/pub/smoke",
+ "//third_party/dart-pkg/pub/source_span",
+ "//third_party/dart-pkg/pub/utf",
+ "//third_party/dart-pkg/pub/path",
+ ]
+}
diff --git a/pub/observe/CHANGELOG.md b/pub/observe/CHANGELOG.md
new file mode 100644
index 0000000..07d54d2
--- /dev/null
+++ b/pub/observe/CHANGELOG.md
@@ -0,0 +1,129 @@
+#### 0.14.0
+
+* Update to be built on top of `package:observable`. Contains the following
+ breaking changes:
+ - `Observable` now lives in `package:observable` and behaves like the old
+ `ChangeNotifier` did (except that it's now the base class) - with subclasses
+ manually notifying listeners of changes via `notifyPropertyChange()`.
+ - `ChangeNotifier` has been removed.
+ - `ObservableList` has been moved to `package:observable`.
+ - `ObservableMap` has been moved to `package:observable`.
+ - `toObservable()` has been moved to `package:observable`.
+ - `Observable` (the one with dirty checking) in `package:observe` has been
+ renamed `AutoObservable`
+
+#### 0.13.5
+
+* Fixed strong mode errors and warnings
+
+#### 0.13.4
+
+* Fixed strong mode errors and warnings
+
+#### 0.13.3+1
+
+* Add support for code_transformers `0.4.x`.
+
+#### 0.13.3
+
+* Update to the `test` package.
+
+#### 0.13.2
+
+* Update to analyzer '^0.27.0'.
+
+#### 0.13.1+3
+
+ * Sorting an already sorted list will no longer yield new change notifications.
+
+#### 0.13.1+2
+
+ * Update to analyzer '<0.27.0'
+
+#### 0.13.1+1
+
+ * Update to logging `<0.12.0`.
+
+#### 0.13.1
+
+ * Update to analyzer `<0.26.0`.
+
+#### 0.13.0+2
+ * Fixed `close` in `PathObserver` so it doesn't leak observers.
+ * Ported the benchmarks from
+ [observe-js](https://github.com/Polymer/observe-js/tree/master/benchmark).
+
+#### 0.13.0+1
+ * Widen the constraint on analyzer.
+
+#### 0.13.0
+ * Don't output log files by default in release mode, and provide option to
+ turn them off entirely.
+ * Changed the api for the ObserveTransformer to use named arguments.
+
+#### 0.12.2+1
+ * Cleanup some method signatures.
+
+#### 0.12.2
+ * Updated to match release 0.5.1
+ [observe-js#d530515](https://github.com/Polymer/observe-js/commit/d530515).
+
+#### 0.12.1+1
+ * Expand stack_trace version constraint.
+
+#### 0.12.1
+ * Upgraded error messages to have a unique and stable identifier.
+
+#### 0.12.0
+ * Old transform.dart file removed. If you weren't use it it, this change is
+ backwards compatible with version 0.11.0.
+
+#### 0.11.0+5
+ * Widen the constraint on analyzer.
+
+#### 0.11.0+4
+ * Raise the lower bound on the source_maps constraint to exclude incompatible
+ versions.
+
+#### 0.11.0+3
+ * Widen the constraint on source_maps.
+
+#### 0.11.0+2
+ * Widen the constraint on barback.
+
+#### 0.11.0+1
+ * Switch from `source_maps`' `Span` class to `source_span`'s `SourceSpan`
+ class.
+
+#### 0.11.0
+ * Updated to match [observe-js#e212e74][e212e74] (release 0.3.4), which also
+ matches [observe-js#fa70c37][fa70c37] (release 0.4.2).
+ * ListPathObserver has been deprecated (it was deleted a while ago in
+ observe-js). We plan to delete it in a future release. You may copy the code
+ if you still need it.
+ * PropertyPath now uses an expression syntax including indexers. For example,
+ you can write `a.b["m"]` instead of `a.b.m`.
+ * **breaking change**: PropertyPath no longer allows numbers as fields, you
+ need to use indexers instead. For example, you now need to write `a[3].d`
+ instead of `a.3.d`.
+ * **breaking change**: PathObserver.value= no longer discards changes (this is
+ in combination with a change in template_binding and polymer to improve
+ interop with JS custom elements).
+
+#### 0.10.0+3
+ * minor changes to documentation, deprecated `discardListChages` in favor of
+ `discardListChanges` (the former had a typo).
+
+#### 0.10.0
+ * package:observe no longer declares @MirrorsUsed. The package uses mirrors
+ for development time, but assumes frameworks (like polymer) and apps that
+ use it directly will either generate code that replaces the use of mirrors,
+ or add the @MirrorsUsed declaration themselves. For convinience, you can
+ import 'package:observe/mirrors_used.dart', and that will add a @MirrorsUsed
+ annotation that preserves properties and classes labeled with @reflectable
+ and properties labeled with @observable.
+ * Updated to match [observe-js#0152d54][0152d54]
+
+[fa70c37]: https://github.com/Polymer/observe-js/blob/fa70c37099026225876f7c7a26bdee7c48129f1c/src/observe.js
+[0152d54]: https://github.com/Polymer/observe-js/blob/0152d542350239563d0f2cad39d22d3254bd6c2a/src/observe.js
+[e212e74]: https://github.com/Polymer/observe-js/blob/e212e7473962067c099a3d1859595c2f8baa36d7/src/observe.js
diff --git a/pub/observe/LICENSE b/pub/observe/LICENSE
new file mode 100644
index 0000000..ee99930
--- /dev/null
+++ b/pub/observe/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2013, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pub/observe/PATENTS b/pub/observe/PATENTS
new file mode 100644
index 0000000..6954196
--- /dev/null
+++ b/pub/observe/PATENTS
@@ -0,0 +1,23 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Dart Project.
+
+Google hereby grants to you a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this
+section) patent license to make, have made, use, offer to sell, sell,
+import, transfer, and otherwise run, modify and propagate the contents
+of this implementation of Dart, where such license applies only to
+those patent claims, both currently owned by Google and acquired in
+the future, licensable by Google that are necessarily infringed by
+this implementation of Dart. This grant does not include claims that
+would be infringed only as a consequence of further modification of
+this implementation. If you or your agent or exclusive licensee
+institute or order or agree to the institution of patent litigation
+against any entity (including a cross-claim or counterclaim in a
+lawsuit) alleging that this implementation of Dart or any code
+incorporated within this implementation of Dart constitutes direct or
+contributory patent infringement, or inducement of patent
+infringement, then any patent rights granted to you under this License
+for this implementation of Dart shall terminate as of the date such
+litigation is filed.
diff --git a/pub/observe/README.md b/pub/observe/README.md
new file mode 100644
index 0000000..9e641b5
--- /dev/null
+++ b/pub/observe/README.md
@@ -0,0 +1,65 @@
+# observe
+
+Support for marking objects as observable, and getting notifications when those
+objects are mutated.
+
+This library is used to observe changes to [AutoObservable][] types. It also
+has helpers to make implementing and using [Observable][] objects easy.
+
+You can provide an observable object in two ways. The simplest way is to
+use dirty checking to discover changes automatically:
+
+```dart
+import 'package:observe/observe.dart';
+import 'package:observe/mirrors_used.dart'; // for smaller code
+
+class Monster extends Unit with AutoObservable {
+ @observable int health = 100;
+
+ void damage(int amount) {
+ print('$this takes $amount damage!');
+ health -= amount;
+ }
+
+ toString() => 'Monster with $health hit points';
+}
+
+main() {
+ var obj = new Monster();
+ obj.changes.listen((records) {
+ print('Changes to $obj were: $records');
+ });
+ // No changes are delivered until we check for them
+ obj.damage(10);
+ obj.damage(20);
+ print('dirty checking!');
+ Observable.dirtyCheck();
+ print('done!');
+}
+```
+
+**Note**: by default this package uses mirrors to access getters and setters
+marked with `@reflectable`. Dart2js disables tree-shaking if there are any
+uses of mirrors, unless you declare how mirrors are used (via the
+[MirrorsUsed](https://api.dartlang.org/apidocs/channels/stable/#dart-mirrors.MirrorsUsed)
+annotation).
+
+As of version 0.10.0, this package doesn't declare `@MirrorsUsed`. This is
+because we intend to use mirrors for development time, but assume that
+frameworks and apps that use this pacakge will either generate code that
+replaces the use of mirrors, or add the `@MirrorsUsed` declaration
+themselves. For convenience, you can import
+`package:observe/mirrors_used.dart` as shown on the first example above.
+That will add a `@MirrorsUsed` annotation that preserves properties and
+classes labeled with `@reflectable` and properties labeled with
+`@observable`.
+
+If you are using the `package:observe/mirrors_used.dart` import, you can
+also make use of `@reflectable` on your own classes and dart2js will
+preserve all of its members for reflection.
+
+[Tools](https://www.dartlang.org/polymer-dart/) exist to convert the first
+form into the second form automatically, to get the best of both worlds.
+
+[AutoObservable]: http://www.dartdocs.org/documentation/observe/latest/index.html#observe/observe.AutoObservable
+[AutoObservable.dirtyCheck]: http://www.dartdocs.org/documentation/observe/latest/index.html#observe/observe.AutoObservable@id_dirtyCheck
diff --git a/pub/observe/codereview.settings b/pub/observe/codereview.settings
new file mode 100644
index 0000000..ade7e33
--- /dev/null
+++ b/pub/observe/codereview.settings
@@ -0,0 +1,3 @@
+CODE_REVIEW_SERVER: http://codereview.chromium.org/
+VIEW_VC: https://github.com/dart-lang/observe/commit/
+CC_LIST: reviews@dartlang.org
diff --git a/pub/observe/lib/html.dart b/pub/observe/lib/html.dart
new file mode 100644
index 0000000..e29b0db
--- /dev/null
+++ b/pub/observe/lib/html.dart
@@ -0,0 +1,83 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// TODO(jmesserly): can we handle this more elegantly?
+// In general, it seems like we want a convenient way to take a Stream plus a
+// getter and convert this into an AutoObservable.
+
+/// Helpers for exposing dart:html as observable data.
+@Deprecated('Parts of Observe used to support Polymer will move out of library')
+library observe.html;
+
+import 'dart:html';
+
+import 'package:observable/observable.dart';
+
+import 'observe.dart';
+
+/// An observable version of [window.location.hash].
+final ObservableLocationHash windowLocation = new ObservableLocationHash._();
+
+class ObservableLocationHash extends PropertyChangeNotifier {
+ Object _currentHash;
+
+ ObservableLocationHash._() {
+ // listen on changes to #hash in the URL
+ // Note: listen on both popState and hashChange, because IE9 doesn't support
+ // history API. See http://dartbug.com/5483
+ // TODO(jmesserly): only listen to these if someone is listening to our
+ // changes.
+ window.onHashChange.listen(_notifyHashChange);
+ window.onPopState.listen(_notifyHashChange);
+
+ _currentHash = hash;
+ }
+
+ @reflectable
+ String get hash => window.location.hash;
+
+ /// Pushes a new URL state, similar to the affect of clicking a link.
+ /// Has no effect if the [value] already equals [window.location.hash].
+ @reflectable
+ void set hash(String value) {
+ if (value == hash) return;
+
+ window.history.pushState(null, '', value);
+ _notifyHashChange(null);
+ }
+
+ void _notifyHashChange(Event _) {
+ var oldValue = _currentHash;
+ _currentHash = hash;
+ notifyPropertyChange(#hash, oldValue, _currentHash);
+ }
+}
+
+/// *Deprecated* use [CssClassSet.toggle] instead.
+///
+/// Add or remove CSS class [className] based on the [value].
+@deprecated
+void updateCssClass(Element element, String className, bool value) {
+ if (value == true) {
+ element.classes.add(className);
+ } else {
+ element.classes.remove(className);
+ }
+}
+
+/// *Deprecated* use `class="{{ binding }}"` in your HTML instead. It will also
+/// work on a `<polymer-element>`.
+///
+/// Bind a CSS class to the observable [object] and property [path].
+@deprecated
+PathObserver bindCssClass(
+ Element element, String className, AutoObservable object, String path) {
+ callback(value) {
+ updateCssClass(element, className, value);
+ }
+
+ var obs = new PathObserver(object, path);
+ callback(obs.open(callback));
+ return obs;
+}
diff --git a/pub/observe/lib/mirrors_used.dart b/pub/observe/lib/mirrors_used.dart
new file mode 100644
index 0000000..3710359
--- /dev/null
+++ b/pub/observe/lib/mirrors_used.dart
@@ -0,0 +1,20 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// An empty library that declares what needs to be retained with [MirrorsUsed]
+/// if you were to use observables together with mirrors. By default this is not
+/// included because frameworks using this package also use code generation to
+/// avoid using mirrors at deploy time.
+@Deprecated('Parts of Observe used to support Polymer will move out of library')
+library observe.mirrors_used;
+
+// Note: ObservableProperty is in this list only for the unusual use case of
+// invoking dart2js without running this package's transformers. The
+// transformer in `lib/transformer.dart` will replace @observable with the
+// @reflectable annotation.
+@MirrorsUsed(
+ metaTargets: const [Reflectable, ObservableProperty],
+ override: 'smoke.mirrors')
+import 'dart:mirrors' show MirrorsUsed;
+import 'package:observe/observe.dart' show Reflectable, ObservableProperty;
diff --git a/pub/observe/lib/observe.dart b/pub/observe/lib/observe.dart
new file mode 100644
index 0000000..b43db31
--- /dev/null
+++ b/pub/observe/lib/observe.dart
@@ -0,0 +1,20 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observe;
+
+// This library contains code ported from observe-js:
+// https://github.com/Polymer/observe-js/blob/0152d542350239563d0f2cad39d22d3254bd6c2a/src/observe.js
+// We port what is needed for data bindings. Most of the functionality is
+// ported, except where differences are needed for Dart's AutoObservable type.
+
+export 'package:observable/observable.dart';
+export 'src/auto_observable.dart';
+export 'src/bindable.dart';
+export 'src/bind_property.dart';
+export 'src/list_path_observer.dart';
+export 'src/metadata.dart';
+export 'src/observable_box.dart';
+export 'src/observer_transform.dart';
+export 'src/path_observer.dart' hide getSegmentsOfPropertyPathForTesting;
diff --git a/pub/observe/lib/src/auto_observable.dart b/pub/observe/lib/src/auto_observable.dart
new file mode 100644
index 0000000..2fae812
--- /dev/null
+++ b/pub/observe/lib/src/auto_observable.dart
@@ -0,0 +1,155 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observe.src.auto_observable;
+
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:observable/observable.dart';
+import 'package:smoke/smoke.dart' as smoke;
+
+import 'dirty_check.dart' show dirtyCheckObservables, registerObservable;
+import 'metadata.dart' show ObservableProperty;
+
+abstract class AutoObservable implements ChangeNotifier {
+ /// Performs dirty checking of objects that inherit from [AutoObservable].
+ /// This scans all observed objects using mirrors and determines if any fields
+ /// have changed. If they have, it delivers the changes for the object.
+ static void dirtyCheck() => dirtyCheckObservables();
+
+ StreamController<List<ChangeRecord>> _changes;
+
+ Map<Symbol, Object> _values;
+ List<ChangeRecord> _records;
+
+ /// The stream of change records to this object. Records will be delivered
+ /// asynchronously.
+ ///
+ /// [deliverChanges] can be called to force synchronous delivery.
+ @override
+ Stream<List<ChangeRecord>> get changes {
+ if (_changes == null) {
+ _changes = new StreamController.broadcast(
+ sync: true, onListen: observed, onCancel: unobserved);
+ }
+ return _changes.stream;
+ }
+
+ /// True if this object has any observers, and should call
+ /// [notifyChange] for changes.
+ @override
+ bool get hasObservers => _changes != null && _changes.hasListener;
+
+ @override
+ void observed() {
+ // Register this object for dirty checking purposes.
+ registerObservable(this);
+
+ var values = new Map<Symbol, Object>();
+
+ // Note: we scan for @observable regardless of whether the base type
+ // actually includes this mixin. While perhaps too inclusive, it lets us
+ // avoid complex logic that walks "with" and "implements" clauses.
+ var queryOptions = new smoke.QueryOptions(
+ includeInherited: true,
+ includeProperties: false,
+ withAnnotations: const [ObservableProperty]);
+ for (var decl in smoke.query(this.runtimeType, queryOptions)) {
+ var name = decl.name;
+ // Note: since this is a field, getting the value shouldn't execute
+ // user code, so we don't need to worry about errors.
+ values[name] = smoke.read(this, name);
+ }
+
+ _values = values;
+ }
+
+ /// Release data associated with observation.
+ @override
+ void unobserved() {
+ // Note: we don't need to explicitly unregister from the dirty check list.
+ // This will happen automatically at the next call to dirtyCheck.
+ if (_values != null) {
+ _values = null;
+ }
+ }
+
+ /// Synchronously deliver pending [changes]. Returns true if any records were
+ /// delivered, otherwise false.
+ // TODO(jmesserly): this is a bit different from the ES Harmony version, which
+ // allows delivery of changes to a particular observer:
+ // http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchangerecords
+ //
+ // The rationale for that, and for async delivery in general, is the principal
+ // that you shouldn't run code (observers) when it doesn't expect to be run.
+ // If you do that, you risk violating invariants that the code assumes.
+ //
+ // For this reason, we need to match the ES Harmony version. The way we can do
+ // this in Dart is to add a method on StreamSubscription (possibly by
+ // subclassing Stream* types) that immediately delivers records for only
+ // that subscription. Alternatively, we could consider using something other
+ // than Stream to deliver the multicast change records, and provide an
+ // Observable->Stream adapter.
+ //
+ // Also: we should be delivering changes to the observer (subscription) based
+ // on the birth order of the observer. This is for compatibility with ES
+ // Harmony as well as predictability for app developers.
+ @override
+ bool deliverChanges() {
+ if (_values == null || !hasObservers) return false;
+
+ // Start with manually notified records (computed properties, etc),
+ // then scan all fields for additional changes.
+ var records = _records;
+ _records = null;
+
+ _values.forEach((name, oldValue) {
+ var newValue = smoke.read(this, name);
+ if (oldValue != newValue) {
+ if (records == null) records = [];
+ records.add(new PropertyChangeRecord(this, name, oldValue, newValue));
+ _values[name] = newValue;
+ }
+ });
+
+ if (records == null) return false;
+
+ _changes.add(new UnmodifiableListView<ChangeRecord>(records));
+ return true;
+ }
+
+ /// Notify that the field [name] of this object has been changed.
+ ///
+ /// The [oldValue] and [newValue] are also recorded. If the two values are
+ /// equal, no change will be recorded.
+ ///
+ /// For convenience this returns [newValue].
+ @override
+ /*=T*/ notifyPropertyChange/*<T>*/(
+ Symbol field, /*=T*/ oldValue, /*=T*/ newValue) {
+ if (hasObservers && oldValue != newValue) {
+ notifyChange(new PropertyChangeRecord(this, field, oldValue, newValue));
+ }
+ return newValue;
+ }
+
+ /// Notify observers of a change.
+ ///
+ /// For most objects [AutoObservable.notifyPropertyChange] is more
+ /// convenient, but collections sometimes deliver other types of changes
+ /// such as a [ListChangeRecord].
+ ///
+ /// Notes:
+ /// - This is *not* required for fields if you mixin or extend
+ /// [AutoObservable], but you can use it for computed properties.
+ /// - Unlike [Observable] this will not schedule [deliverChanges]; use
+ /// [AutoObservable.dirtyCheck] instead.
+ @override
+ void notifyChange([ChangeRecord record]) {
+ if (record == null || !hasObservers) return;
+ if (_records == null) _records = [];
+ _records.add(record);
+ }
+}
diff --git a/pub/observe/lib/src/bind_property.dart b/pub/observe/lib/src/bind_property.dart
new file mode 100644
index 0000000..7f1df91
--- /dev/null
+++ b/pub/observe/lib/src/bind_property.dart
@@ -0,0 +1,38 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observe.src.bind_property;
+
+import 'dart:async';
+import 'package:observable/observable.dart' as obs;
+
+/// Forwards an observable property from one object to another. For example:
+///
+/// class MyModel extends AutoObservable {
+/// StreamSubscription _sub;
+/// MyOtherModel _otherModel;
+///
+/// MyModel() {
+/// ...
+/// _sub = onPropertyChange(_otherModel, #value,
+/// () => notifyPropertyChange(#prop, oldValue, newValue);
+/// }
+///
+/// String get prop => _otherModel.value;
+/// set prop(String value) { _otherModel.value = value; }
+/// }
+///
+/// See also [notifyPropertyChange].
+// TODO(jmesserly): make this an instance method?
+StreamSubscription onPropertyChange(
+ obs.Observable source, Symbol sourceName, void callback()) {
+ return source.changes.listen((records) {
+ for (var record in records) {
+ if (record is obs.PropertyChangeRecord && record.name == sourceName) {
+ callback();
+ break;
+ }
+ }
+ });
+}
diff --git a/pub/observe/lib/src/bindable.dart b/pub/observe/lib/src/bindable.dart
new file mode 100644
index 0000000..c3314a2
--- /dev/null
+++ b/pub/observe/lib/src/bindable.dart
@@ -0,0 +1,42 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observe.src.bindable;
+
+/// An object that can be data bound.
+// Normally this is used with 'package:template_binding'.
+// TODO(jmesserly): Node.bind polyfill calls this "observable"
+abstract class Bindable {
+ // Dart note: changed setValue to be "set value" and discardChanges() to
+ // be "get value".
+
+ /// Initiates observation and returns the initial value.
+ /// The callback will be called with the updated [value].
+ ///
+ /// Some subtypes may chose to provide additional arguments, such as
+ /// [PathObserver] providing the old value as the second argument.
+ /// However, they must support callbacks with as few as 0 or 1 argument.
+ /// This can be implemented by performing an "is" type test on the callback.
+ open(callback);
+
+ /// Stops future notifications and frees the reference to the callback passed
+ /// to [open], so its memory can be collected even if this Bindable is alive.
+ void close();
+
+ /// Gets the current value of the bindings.
+ /// Note: once the value of a [Bindable] is fetched, the callback passed to
+ /// [open] should not be called again with this new value.
+ /// In other words, any pending change notifications must be discarded.
+ // TODO(jmesserly): I don't like a getter with side effects. Should we just
+ // rename the getter/setter pair to discardChanges/setValue like they are in
+ // JavaScript?
+ get value;
+
+ /// This can be implemented for two-way bindings. By default does nothing.
+ set value(newValue) {}
+
+ /// Deliver changes. Typically this will perform dirty-checking, if any is
+ /// needed.
+ void deliver() {}
+}
diff --git a/pub/observe/lib/src/dirty_check.dart b/pub/observe/lib/src/dirty_check.dart
new file mode 100644
index 0000000..c60753c
--- /dev/null
+++ b/pub/observe/lib/src/dirty_check.dart
@@ -0,0 +1,136 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// *Warning*: this library is **internal**, and APIs are subject to change.
+///
+/// Tracks observable objects for dirty checking and testing purposes.
+///
+/// It can collect all observed objects, which can be used to trigger
+/// predictable delivery of all pending changes in a test, including objects
+/// allocated internally to another library, such as those in
+/// `package:template_binding`.
+library observe.src.dirty_check;
+
+import 'dart:async';
+
+import 'package:func/func.dart';
+import 'package:logging/logging.dart';
+import 'package:observable/observable.dart' as obs show Observable;
+
+import 'auto_observable.dart' show AutoObservable;
+
+/// The number of active observables in the system.
+int get allObservablesCount => _allObservablesCount;
+
+int _allObservablesCount = 0;
+
+List<obs.Observable> _allObservables = null;
+
+bool _delivering = false;
+
+void registerObservable(obs.Observable obj) {
+ if (_allObservables == null) _allObservables = <obs.Observable>[];
+ _allObservables.add(obj);
+ _allObservablesCount++;
+}
+
+/// Synchronously deliver all change records for known observables.
+///
+/// This will execute [AutoObservable.deliverChanges] on objects that inherit from
+/// [AutoObservable].
+// Note: this is called performMicrotaskCheckpoint in change_summary.js.
+void dirtyCheckObservables() {
+ if (_delivering) return;
+ if (_allObservables == null) return;
+
+ _delivering = true;
+
+ int cycles = 0;
+ bool anyChanged = false;
+ List debugLoop = null;
+ do {
+ cycles++;
+ if (cycles == MAX_DIRTY_CHECK_CYCLES) {
+ debugLoop = [];
+ }
+
+ var toCheck = _allObservables;
+ _allObservables = <obs.Observable>[];
+ anyChanged = false;
+
+ for (int i = 0; i < toCheck.length; i++) {
+ final observer = toCheck[i];
+ if (observer.hasObservers) {
+ if (observer.deliverChanges()) {
+ anyChanged = true;
+ if (debugLoop != null) debugLoop.add([i, observer]);
+ }
+ _allObservables.add(observer);
+ }
+ }
+ } while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged);
+
+ if (debugLoop != null && anyChanged) {
+ _logger.warning('Possible loop in AutoObservable.dirtyCheck, stopped '
+ 'checking.');
+ for (final info in debugLoop) {
+ _logger.warning('In last iteration AutoObservable changed at index '
+ '${info[0]}, object: ${info[1]}.');
+ }
+ }
+
+ _allObservablesCount = _allObservables.length;
+ _delivering = false;
+}
+
+const MAX_DIRTY_CHECK_CYCLES = 1000;
+
+/// Log for messages produced at runtime by this library. Logging can be
+/// configured by accessing Logger.root from the logging library.
+final Logger _logger = new Logger('AutoObservable.dirtyCheck');
+
+/// Creates a [ZoneSpecification] to set up automatic dirty checking after each
+/// batch of async operations. This ensures that change notifications are always
+/// delivered. Typically used via [dirtyCheckZone].
+ZoneSpecification dirtyCheckZoneSpec() {
+ bool pending = false;
+
+ enqueueDirtyCheck(ZoneDelegate parent, Zone zone) {
+ // Only schedule one dirty check per microtask.
+ if (pending) return;
+
+ pending = true;
+ parent.scheduleMicrotask(zone, () {
+ pending = false;
+ AutoObservable.dirtyCheck();
+ });
+ }
+
+ Func0 wrapCallback(Zone self, ZoneDelegate parent, Zone zone, f()) {
+ // TODO(jmesserly): why does this happen?
+ if (f == null) return f;
+ return () {
+ enqueueDirtyCheck(parent, zone);
+ return f();
+ };
+ }
+
+ Func1 wrapUnaryCallback(Zone self, ZoneDelegate parent, Zone zone, f(x)) {
+ // TODO(jmesserly): why does this happen?
+ if (f == null) return f;
+ return (x) {
+ enqueueDirtyCheck(parent, zone);
+ return f(x);
+ };
+ }
+
+ return new ZoneSpecification(
+ registerCallback: wrapCallback, registerUnaryCallback: wrapUnaryCallback);
+}
+
+/// Forks a [Zone] off the current one that does dirty-checking automatically
+/// after each batch of async operations. Equivalent to:
+///
+/// Zone.current.fork(specification: dirtyCheckZoneSpec());
+Zone dirtyCheckZone() => Zone.current.fork(specification: dirtyCheckZoneSpec());
diff --git a/pub/observe/lib/src/list_path_observer.dart b/pub/observe/lib/src/list_path_observer.dart
new file mode 100644
index 0000000..c329103
--- /dev/null
+++ b/pub/observe/lib/src/list_path_observer.dart
@@ -0,0 +1,73 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observe.src.list_path_observer;
+
+import 'dart:async';
+import 'package:observable/observable.dart';
+import 'package:observe/observe.dart';
+
+// Inspired by ArrayReduction at:
+// https://raw.github.com/rafaelw/ChangeSummary/master/util/array_reduction.js
+// The main difference is we support anything on the rich Dart Iterable API.
+
+/// Observes a path starting from each item in the list.
+@deprecated
+class ListPathObserver<E, P> extends PropertyChangeNotifier {
+ final ObservableList<E> list;
+ final String _itemPath;
+ final List<PathObserver> _observers = <PathObserver>[];
+ StreamSubscription _sub;
+ bool _scheduled = false;
+ Iterable<P> _value;
+
+ ListPathObserver(this.list, String path) : _itemPath = path {
+ // TODO(jmesserly): delay observation until we are observed.
+ _sub = list.listChanges.listen((records) {
+ for (var record in records) {
+ _observeItems(record.addedCount - record.removed.length);
+ }
+ _scheduleReduce(null);
+ });
+
+ _observeItems(list.length);
+ _reduce();
+ }
+
+ @reflectable
+ Iterable<P> get value => _value;
+
+ void dispose() {
+ if (_sub != null) _sub.cancel();
+ _observers.forEach((o) => o.close());
+ _observers.clear();
+ }
+
+ void _reduce() {
+ _scheduled = false;
+ var newValue = _observers.map((o) => o.value as P);
+ _value = notifyPropertyChange(#value, _value, newValue);
+ }
+
+ void _scheduleReduce(_) {
+ if (_scheduled) return;
+ _scheduled = true;
+ scheduleMicrotask(_reduce);
+ }
+
+ void _observeItems(int lengthAdjust) {
+ if (lengthAdjust > 0) {
+ for (int i = 0; i < lengthAdjust; i++) {
+ int len = _observers.length;
+ var pathObs = new PathObserver(list, '[$len].$_itemPath');
+ pathObs.open(_scheduleReduce);
+ _observers.add(pathObs);
+ }
+ } else if (lengthAdjust < 0) {
+ for (int i = 0; i < -lengthAdjust; i++) {
+ _observers.removeLast().close();
+ }
+ }
+ }
+}
diff --git a/pub/observe/lib/src/messages.dart b/pub/observe/lib/src/messages.dart
new file mode 100644
index 0000000..f2c46f5
--- /dev/null
+++ b/pub/observe/lib/src/messages.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains all warning messages produced by the observe transformer.
+library observe.src.messages;
+
+import 'package:code_transformers/messages/messages.dart';
+
+const NO_OBSERVABLE_ON_LIBRARY = const MessageTemplate(
+ const MessageId('observe', 1),
+ '@observable on a library no longer has any effect. '
+ 'Instead, annotate individual fields as @observable.',
+ '`@observable` not supported on libraries',
+ _COMMON_MESSAGE_WHERE_TO_USE_OBSERVABLE);
+
+const NO_OBSERVABLE_ON_TOP_LEVEL = const MessageTemplate(
+ const MessageId('observe', 2),
+ 'Top-level fields can no longer be observable. '
+ 'Observable fields must be in observable objects.',
+ '`@observable` not supported on top-level fields',
+ _COMMON_MESSAGE_WHERE_TO_USE_OBSERVABLE);
+
+const NO_OBSERVABLE_ON_CLASS = const MessageTemplate(
+ const MessageId('observe', 3),
+ '@observable on a class no longer has any effect. '
+ 'Instead, annotate individual fields as @observable.',
+ '`@observable` not supported on classes',
+ _COMMON_MESSAGE_WHERE_TO_USE_OBSERVABLE);
+
+const NO_OBSERVABLE_ON_STATIC_FIELD = const MessageTemplate(
+ const MessageId('observe', 4),
+ 'Static fields can no longer be observable. '
+ 'Observable fields must be in observable objects.',
+ '`@observable` not supported on static fields',
+ _COMMON_MESSAGE_WHERE_TO_USE_OBSERVABLE);
+
+const REQUIRE_OBSERVABLE_INTERFACE = const MessageTemplate(
+ const MessageId('observe', 5),
+ 'Observable fields must be in observable objects. '
+ 'Change this class to extend, mix in, or implement AutoObservable.',
+ '`@observable` field not in an `AutoObservable` class',
+ _COMMON_MESSAGE_WHERE_TO_USE_OBSERVABLE);
+
+const String _COMMON_MESSAGE_WHERE_TO_USE_OBSERVABLE = '''
+Only instance fields on `Observable` classes can be observable,
+and you must explicitly annotate each observable field as `@observable`.
+
+Support for using the `@observable` annotation in libraries, classes, and
+elsewhere is deprecated.
+''';
diff --git a/pub/observe/lib/src/metadata.dart b/pub/observe/lib/src/metadata.dart
new file mode 100644
index 0000000..15e7905
--- /dev/null
+++ b/pub/observe/lib/src/metadata.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observe.src.metadata;
+
+/// Use `@observable` to make a field automatically observable, or to indicate
+/// that a property is observable. This only works on classes that extend or
+/// mix in `AutoObservable`.
+const ObservableProperty observable = const ObservableProperty();
+
+/// An annotation that is used to make a property observable.
+/// Normally this is used via the [observable] constant, for example:
+///
+/// class Monster extends AutoObservable {
+/// @observable int health;
+/// }
+///
+// TODO(sigmund): re-add this to the documentation when it's really true:
+// If needed, you can subclass this to create another annotation that will
+// also be treated as observable.
+// Note: observable properties imply reflectable.
+class ObservableProperty {
+ const ObservableProperty();
+}
+
+/// This can be used to retain any properties that you wish to access with
+/// Dart's mirror system. If you import `package:observe/mirrors_used.dart`, all
+/// classes or members annotated with `@reflectable` wil be preserved by dart2js
+/// during compilation. This is necessary to make the member visible to
+/// `PathObserver`, or similar systems, once the code is deployed, if you are
+/// not doing a different kind of code-generation for your app. If you are using
+/// polymer, you most likely don't need to use this annotation anymore.
+const Reflectable reflectable = const Reflectable();
+
+/// An annotation that is used to make a type or member reflectable. This makes
+/// it available to `PathObserver` at runtime. For example:
+///
+/// @reflectable
+/// class Monster extends AutoObservable {
+/// int _health;
+/// int get health => _health;
+/// ...
+/// }
+/// ...
+/// // This will work even if the code has been tree-shaken/minified:
+/// final monster = new Monster();
+/// new PathObserver(monster, 'health').changes.listen(...);
+class Reflectable {
+ const Reflectable();
+}
diff --git a/pub/observe/lib/src/observable_box.dart b/pub/observe/lib/src/observable_box.dart
new file mode 100644
index 0000000..5749890
--- /dev/null
+++ b/pub/observe/lib/src/observable_box.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observe.src.observable_box;
+
+import 'package:observable/observable.dart';
+import 'package:observe/observe.dart';
+
+// TODO(jmesserly): should the property name be configurable?
+// That would be more convenient.
+/// An observable box that holds a value. Use this if you want to store a single
+/// value. For other cases, it is better to use [AutoObservableList],
+/// [AutoObservableMap], or a custom [AutoObservable] implementation based on
+/// [AutoObservable]. The property name for changes is "value".
+class ObservableBox<T> extends PropertyChangeNotifier {
+ T _value;
+
+ ObservableBox([T initialValue]) : _value = initialValue;
+
+ @reflectable
+ T get value => _value;
+
+ @reflectable
+ void set value(T newValue) {
+ _value = notifyPropertyChange(#value, _value, newValue);
+ }
+
+ String toString() => '#<$runtimeType value: $value>';
+}
diff --git a/pub/observe/lib/src/observer_transform.dart b/pub/observe/lib/src/observer_transform.dart
new file mode 100644
index 0000000..735e3ab
--- /dev/null
+++ b/pub/observe/lib/src/observer_transform.dart
@@ -0,0 +1,85 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observe.src.observer_transform;
+
+import 'package:observe/observe.dart';
+
+/// ObserverTransform is used to dynamically transform observed value(s).
+///
+/// var obj = new ObservableBox(10);
+/// var observer = new PathObserver(obj, 'value');
+/// var transform = new ObserverTransform(observer,
+/// (x) => x * 2, setValue: (x) => x ~/ 2);
+///
+/// // Open returns the current value of 20.
+/// transform.open((newValue) => print('new: $newValue'));
+///
+/// obj.value = 20; // prints 'new: 40' async
+/// new Future(() {
+/// transform.value = 4; // obj.value will be 2
+/// });
+///
+/// ObserverTransform can also be used to reduce a set of observed values to a
+/// single value:
+///
+/// var obj = new AutoObservableMap.from({'a': 1, 'b': 2, 'c': 3});
+/// var observer = new CompoundObserver()
+/// ..addPath(obj, 'a')
+/// ..addPath(obj, 'b')
+/// ..addPath(obj, 'c');
+///
+/// var transform = new ObserverTransform(observer,
+/// (values) => values.fold(0, (x, y) => x + y));
+///
+/// // Open returns the current value of 6.
+/// transform.open((newValue) => print('new: $newValue'));
+///
+/// obj['a'] = 2;
+/// obj['c'] = 10; // will print 'new 14' asynchronously
+///
+class ObserverTransform extends Bindable {
+ Bindable _bindable;
+ Function _getTransformer, _setTransformer;
+ Function _notifyCallback;
+ var _value;
+
+ ObserverTransform(Bindable bindable, computeValue(value), {setValue(value)})
+ : _bindable = bindable,
+ _getTransformer = computeValue,
+ _setTransformer = setValue;
+
+ open(callback) {
+ _notifyCallback = callback;
+ _value = _getTransformer(_bindable.open(_observedCallback));
+ return _value;
+ }
+
+ _observedCallback(newValue) {
+ final value = _getTransformer(newValue);
+ if (value == _value) return null;
+ _value = value;
+ return _notifyCallback(value);
+ }
+
+ void close() {
+ if (_bindable != null) _bindable.close();
+ _bindable = null;
+ _getTransformer = null;
+ _setTransformer = null;
+ _notifyCallback = null;
+ _value = null;
+ }
+
+ get value => _value = _getTransformer(_bindable.value);
+
+ set value(newValue) {
+ if (_setTransformer != null) {
+ newValue = _setTransformer(newValue);
+ }
+ _bindable.value = newValue;
+ }
+
+ deliver() => _bindable.deliver();
+}
diff --git a/pub/observe/lib/src/path_observer.dart b/pub/observe/lib/src/path_observer.dart
new file mode 100644
index 0000000..a8e7868
--- /dev/null
+++ b/pub/observe/lib/src/path_observer.dart
@@ -0,0 +1,921 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library observe.src.path_observer;
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:math' show min;
+
+import 'package:logging/logging.dart' show Logger, Level;
+import 'package:observable/observable.dart';
+import 'package:observe/observe.dart';
+import 'package:smoke/smoke.dart' as smoke;
+
+import 'package:utf/utf.dart' show stringToCodepoints;
+
+/// A data-bound path starting from a view-model or model object, for example
+/// `foo.bar.baz`.
+///
+/// When [open] is called, this will observe changes to the object and any
+/// intermediate object along the path, and send updated values accordingly.
+/// When [close] is called it will stop observing the objects.
+///
+/// This class is used to implement `Node.bind` and similar functionality in
+/// the [template_binding](pub.dartlang.org/packages/template_binding) package.
+class PathObserver extends _Observer implements Bindable {
+ PropertyPath _path;
+ Object _object;
+ _ObservedSet _directObserver;
+
+ /// Observes [path] on [object] for changes. This returns an object
+ /// that can be used to get the changes and get/set the value at this path.
+ ///
+ /// The path can be a [PropertyPath], or a [String] used to construct it.
+ ///
+ /// See [open] and [value].
+ PathObserver(Object object, [path])
+ : _object = object,
+ _path = new PropertyPath(path);
+
+ PropertyPath get path => _path;
+
+ /// Sets the value at this path.
+ void set value(newValue) {
+ if (_path != null) _path.setValueFrom(_object, newValue);
+ }
+
+ int get _reportArgumentCount => 2;
+
+ /// Initiates observation and returns the initial value.
+ /// The callback will be passed the updated [value], and may optionally be
+ /// declared to take a second argument, which will contain the previous value.
+ open(callback) => super.open(callback);
+
+ void _connect() {
+ _directObserver = new _ObservedSet(this, _object);
+ _check(skipChanges: true);
+ }
+
+ void _disconnect() {
+ _value = null;
+ if (_directObserver != null) {
+ _directObserver.close(this);
+ _directObserver = null;
+ }
+ // Dart note: the JS impl does not do this, but it seems consistent with
+ // CompoundObserver. After closing the PathObserver can't be reopened.
+ _path = null;
+ _object = null;
+ }
+
+ void _iterateObjects(void observe(obj, prop)) {
+ _path._iterateObjects(_object, observe);
+ }
+
+ bool _check({bool skipChanges: false}) {
+ var oldValue = _value;
+ _value = _path.getValueFrom(_object);
+ if (skipChanges || _value == oldValue) return false;
+
+ _report(_value, oldValue, this);
+ return true;
+ }
+}
+
+/// A dot-delimieted property path such as "foo.bar" or "foo.10.bar".
+///
+/// The path specifies how to get a particular value from an object graph, where
+/// the graph can include arrays and maps. Each segment of the path describes
+/// how to take a single step in the object graph. Properties like 'foo' or
+/// 'bar' are read as properties on objects, or as keys if the object is a [Map]
+/// or a [Indexable], while integer values are read as indexes in a [List].
+// TODO(jmesserly): consider specialized subclasses for:
+// * empty path
+// * "value"
+// * single token in path, e.g. "foo"
+class PropertyPath {
+ /// The segments of the path.
+ final List<Object> _segments;
+
+ /// Creates a new [PropertyPath]. These can be stored to avoid excessive
+ /// parsing of path strings.
+ ///
+ /// The provided [path] should be a String or a List. If it is a list it
+ /// should contain only Symbols and integers. This can be used to avoid
+ /// parsing.
+ ///
+ /// Note that this constructor will canonicalize identical paths in some cases
+ /// to save memory, but this is not guaranteed. Use [==] for comparions
+ /// purposes instead of [identical].
+ // Dart note: this is ported from `function getPath`.
+ factory PropertyPath([path]) {
+ if (path is PropertyPath) return path;
+ if (path == null || (path is List && path.isEmpty)) path = '';
+
+ if (path is List) {
+ var copy = new List.from(path, growable: false);
+ for (var segment in copy) {
+ // Dart note: unlike Javascript, we don't support arbitraty objects that
+ // can be converted to a String.
+ // TODO(sigmund): consider whether we should support that here. It might
+ // be easier to add support for that if we switch first to use strings
+ // for everything instead of symbols.
+ if (segment is! int && segment is! String && segment is! Symbol) {
+ throw new ArgumentError(
+ 'List must contain only ints, Strings, and Symbols');
+ }
+ }
+ return new PropertyPath._(copy);
+ }
+
+ var pathObj = _pathCache[path];
+ if (pathObj != null) return pathObj;
+
+ final segments = new _PathParser().parse(path);
+ if (segments == null) return _InvalidPropertyPath._instance;
+
+ // TODO(jmesserly): we could use an UnmodifiableListView here, but that adds
+ // memory overhead.
+ pathObj = new PropertyPath._(segments.toList(growable: false));
+ if (_pathCache.length >= _pathCacheLimit) {
+ _pathCache.remove(_pathCache.keys.first);
+ }
+ _pathCache[path] = pathObj;
+ return pathObj;
+ }
+
+ PropertyPath._(this._segments);
+
+ int get length => _segments.length;
+ bool get isEmpty => _segments.isEmpty;
+ bool get isValid => true;
+
+ String toString() {
+ if (!isValid) return '<invalid path>';
+ var sb = new StringBuffer();
+ bool first = true;
+ for (var key in _segments) {
+ if (key is Symbol) {
+ if (!first) sb.write('.');
+ sb.write(smoke.symbolToName(key));
+ } else {
+ _formatAccessor(sb, key);
+ }
+ first = false;
+ }
+ return sb.toString();
+ }
+
+ _formatAccessor(StringBuffer sb, Object key) {
+ if (key is int) {
+ sb.write('[$key]');
+ } else {
+ sb.write('["${key.toString().replaceAll('"', '\\"')}"]');
+ }
+ }
+
+ bool operator ==(other) {
+ if (identical(this, other)) return true;
+ if (other is! PropertyPath) return false;
+ if (isValid != other.isValid) return false;
+
+ int len = _segments.length;
+ if (len != other._segments.length) return false;
+ for (int i = 0; i < len; i++) {
+ if (_segments[i] != other._segments[i]) return false;
+ }
+ return true;
+ }
+
+ /// This is the [Jenkins hash function][1] but using masking to keep
+ /// values in SMI range.
+ /// [1]: http://en.wikipedia.org/wiki/Jenkins_hash_function
+ // TODO(jmesserly): should reuse this instead, see
+ // https://code.google.com/p/dart/issues/detail?id=11617
+ int get hashCode {
+ int hash = 0;
+ for (int i = 0, len = _segments.length; i < len; i++) {
+ hash = 0x1fffffff & (hash + _segments[i].hashCode);
+ hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
+ hash = hash ^ (hash >> 6);
+ }
+ hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
+ hash = hash ^ (hash >> 11);
+ return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
+ }
+
+ /// Returns the current value of the path from the provided [obj]ect.
+ getValueFrom(Object obj) {
+ if (!isValid) return null;
+ for (var segment in _segments) {
+ if (obj == null) return null;
+ obj = _getObjectProperty(obj, segment);
+ }
+ return obj;
+ }
+
+ /// Attempts to set the [value] of the path from the provided [obj]ect.
+ /// Returns true if and only if the path was reachable and set.
+ bool setValueFrom(Object obj, Object value) {
+ var end = _segments.length - 1;
+ if (end < 0) return false;
+ for (int i = 0; i < end; i++) {
+ if (obj == null) return false;
+ obj = _getObjectProperty(obj, _segments[i]);
+ }
+ return _setObjectProperty(obj, _segments[end], value);
+ }
+
+ void _iterateObjects(Object obj, void observe(obj, prop)) {
+ if (!isValid || isEmpty) return;
+
+ int i = 0, last = _segments.length - 1;
+ while (obj != null) {
+ // _segments[i] is passed to indicate that we are only observing that
+ // property of obj. See observe declaration in _ObservedSet.
+ observe(obj, _segments[i]);
+
+ if (i >= last) break;
+ obj = _getObjectProperty(obj, _segments[i++]);
+ }
+ }
+
+ // Dart note: it doesn't make sense to have compiledGetValueFromFn in Dart.
+}
+
+/// Visible only for testing:
+getSegmentsOfPropertyPathForTesting(p) => p._segments;
+
+class _InvalidPropertyPath extends PropertyPath {
+ static final _instance = new _InvalidPropertyPath();
+
+ bool get isValid => false;
+ _InvalidPropertyPath() : super._([]);
+}
+
+/// Properties in [Map] that need to be read as properties and not as keys in
+/// the map. We exclude methods ('containsValue', 'containsKey', 'putIfAbsent',
+/// 'addAll', 'remove', 'clear', 'forEach') because there is no use in reading
+/// them as part of path-observer segments.
+const _MAP_PROPERTIES = const [#keys, #values, #length, #isEmpty, #isNotEmpty];
+
+_getObjectProperty(object, property) {
+ if (object == null) return null;
+
+ if (property is int) {
+ if (object is List && property >= 0 && property < object.length) {
+ return object[property];
+ }
+ } else if (property is String) {
+ return object[property];
+ } else if (property is Symbol) {
+ // Support indexer if available, e.g. Maps or polymer_expressions Scope.
+ // This is the default syntax used by polymer/nodebind and
+ // polymer/observe-js PathObserver.
+ // TODO(sigmund): should we also support using checking dynamically for
+ // whether the type practically implements the indexer API
+ // (smoke.hasInstanceMethod(type, const Symbol('[]')))?
+ if (object is Indexable ||
+ object is Map && !_MAP_PROPERTIES.contains(property)) {
+ return object[smoke.symbolToName(property)];
+ }
+ try {
+ return smoke.read(object, property);
+ } on NoSuchMethodError catch (_) {
+ // Rethrow, unless the type implements noSuchMethod, in which case we
+ // interpret the exception as a signal that the method was not found.
+ // Dart note: getting invalid properties is an error, unlike in JS where
+ // it returns undefined.
+ if (!smoke.hasNoSuchMethod(object.runtimeType)) rethrow;
+ }
+ }
+
+ if (_logger.isLoggable(Level.FINER)) {
+ _logger.finer("can't get $property in $object");
+ }
+ return null;
+}
+
+bool _setObjectProperty(object, property, value) {
+ if (object == null) return false;
+
+ if (property is int) {
+ if (object is List && property >= 0 && property < object.length) {
+ object[property] = value;
+ return true;
+ }
+ } else if (property is Symbol) {
+ // Support indexer if available, e.g. Maps or polymer_expressions Scope.
+ if (object is Indexable ||
+ object is Map && !_MAP_PROPERTIES.contains(property)) {
+ object[smoke.symbolToName(property)] = value;
+ return true;
+ }
+ try {
+ smoke.write(object, property, value);
+ return true;
+ } on NoSuchMethodError catch (_) {
+ if (!smoke.hasNoSuchMethod(object.runtimeType)) rethrow;
+ }
+ }
+
+ if (_logger.isLoggable(Level.FINER)) {
+ _logger.finer("can't set $property in $object");
+ }
+ return false;
+}
+
+// From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
+
+final RegExp _identRegExp = () {
+ const identStart = '[\$_a-zA-Z]';
+ const identPart = '[\$_a-zA-Z0-9]';
+ return new RegExp('^$identStart+$identPart*\$');
+}();
+
+_isIdent(s) => _identRegExp.hasMatch(s);
+
+// Dart note: refactored to convert to codepoints once and operate on codepoints
+// rather than characters.
+class _PathParser {
+ List<Object> keys = [];
+ int index = -1;
+ String key;
+
+ final Map<String, Map<String, List<String>>> _pathStateMachine = {
+ 'beforePath': {
+ 'ws': ['beforePath'],
+ 'ident': ['inIdent', 'append'],
+ '[': ['beforeElement'],
+ 'eof': ['afterPath']
+ },
+ 'inPath': {
+ 'ws': ['inPath'],
+ '.': ['beforeIdent'],
+ '[': ['beforeElement'],
+ 'eof': ['afterPath']
+ },
+ 'beforeIdent': {
+ 'ws': ['beforeIdent'],
+ 'ident': ['inIdent', 'append']
+ },
+ 'inIdent': {
+ 'ident': ['inIdent', 'append'],
+ '0': ['inIdent', 'append'],
+ 'number': ['inIdent', 'append'],
+ 'ws': ['inPath', 'push'],
+ '.': ['beforeIdent', 'push'],
+ '[': ['beforeElement', 'push'],
+ 'eof': ['afterPath', 'push']
+ },
+ 'beforeElement': {
+ 'ws': ['beforeElement'],
+ '0': ['afterZero', 'append'],
+ 'number': ['inIndex', 'append'],
+ "'": ['inSingleQuote', 'append', ''],
+ '"': ['inDoubleQuote', 'append', '']
+ },
+ 'afterZero': {
+ 'ws': ['afterElement', 'push'],
+ ']': ['inPath', 'push']
+ },
+ 'inIndex': {
+ '0': ['inIndex', 'append'],
+ 'number': ['inIndex', 'append'],
+ 'ws': ['afterElement'],
+ ']': ['inPath', 'push']
+ },
+ 'inSingleQuote': {
+ "'": ['afterElement'],
+ 'eof': ['error'],
+ 'else': ['inSingleQuote', 'append']
+ },
+ 'inDoubleQuote': {
+ '"': ['afterElement'],
+ 'eof': ['error'],
+ 'else': ['inDoubleQuote', 'append']
+ },
+ 'afterElement': {
+ 'ws': ['afterElement'],
+ ']': ['inPath', 'push']
+ }
+ };
+
+ /// From getPathCharType: determines the type of a given [code]point.
+ String _getPathCharType(code) {
+ if (code == null) return 'eof';
+ switch (code) {
+ case 0x5B: // [
+ case 0x5D: // ]
+ case 0x2E: // .
+ case 0x22: // "
+ case 0x27: // '
+ case 0x30: // 0
+ return _char(code);
+
+ case 0x5F: // _
+ case 0x24: // $
+ return 'ident';
+
+ case 0x20: // Space
+ case 0x09: // Tab
+ case 0x0A: // Newline
+ case 0x0D: // Return
+ case 0xA0: // No-break space
+ case 0xFEFF: // Byte Order Mark
+ case 0x2028: // Line Separator
+ case 0x2029: // Paragraph Separator
+ return 'ws';
+ }
+
+ // a-z, A-Z
+ if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A))
+ return 'ident';
+
+ // 1-9
+ if (0x31 <= code && code <= 0x39) return 'number';
+
+ return 'else';
+ }
+
+ static String _char(int codepoint) => new String.fromCharCodes([codepoint]);
+
+ void push() {
+ if (key == null) return;
+
+ // Dart note: we store the keys with different types, rather than
+ // parsing/converting things later in toString.
+ if (_isIdent(key)) {
+ keys.add(smoke.nameToSymbol(key));
+ } else {
+ var index = int.parse(key, radix: 10, onError: (_) => null);
+ keys.add(index != null ? index : key);
+ }
+ key = null;
+ }
+
+ void append(newChar) {
+ key = (key == null) ? newChar : '$key$newChar';
+ }
+
+ bool _maybeUnescapeQuote(String mode, codePoints) {
+ if (index >= codePoints.length) return false;
+ var nextChar = _char(codePoints[index + 1]);
+ if ((mode == 'inSingleQuote' && nextChar == "'") ||
+ (mode == 'inDoubleQuote' && nextChar == '"')) {
+ index++;
+ append(nextChar);
+ return true;
+ }
+ return false;
+ }
+
+ /// Returns the parsed keys, or null if there was a parse error.
+ List<Object> parse(String path) {
+ var codePoints = stringToCodepoints(path);
+ var mode = 'beforePath';
+
+ while (mode != null) {
+ index++;
+ var c = index >= codePoints.length ? null : codePoints[index];
+
+ if (c != null &&
+ _char(c) == '\\' &&
+ _maybeUnescapeQuote(mode, codePoints)) continue;
+
+ var type = _getPathCharType(c);
+ if (mode == 'error') return null;
+
+ var typeMap = _pathStateMachine[mode];
+ var transition = typeMap[type];
+ if (transition == null) transition = typeMap['else'];
+ if (transition == null) return null; // parse error;
+
+ mode = transition[0];
+ var actionName = transition.length > 1 ? transition[1] : null;
+ if (actionName == 'push' && key != null) push();
+ if (actionName == 'append') {
+ var newChar = transition.length > 2 && transition[2] != null
+ ? transition[2]
+ : _char(c);
+ append(newChar);
+ }
+
+ if (mode == 'afterPath') return keys;
+ }
+ return null; // parse error
+ }
+}
+
+final Logger _logger = new Logger('observe.PathObserver');
+
+/// This is a simple cache. It's like LRU but we don't update an item on a
+/// cache hit, because that would require allocation. Better to let it expire
+/// and reallocate the PropertyPath.
+// TODO(jmesserly): this optimization is from observe-js, how valuable is it in
+// practice?
+final _pathCache = new LinkedHashMap<String, PropertyPath>();
+
+/// The size of a path like "foo.bar" is approximately 160 bytes, so this
+/// reserves ~16Kb of memory for recently used paths. Since paths are frequently
+/// reused, the theory is that this ends up being a good tradeoff in practice.
+// (Note: the 160 byte estimate is from Dart VM 1.0.0.10_r30798 on x64 without
+// using UnmodifiableListView in PropertyPath)
+const int _pathCacheLimit = 100;
+
+/// [CompoundObserver] is a [Bindable] object which knows how to listen to
+/// multiple values (registered via [addPath] or [addObserver]) and invoke a
+/// callback when one or more of the values have changed.
+///
+/// var obj = new ObservableMap.from({'a': 1, 'b': 2});
+/// var otherObj = new ObservableMap.from({'c': 3});
+///
+/// var observer = new CompoundObserver()
+/// ..addPath(obj, 'a');
+/// ..addObserver(new PathObserver(obj, 'b'));
+/// ..addPath(otherObj, 'c');
+/// ..open((values) {
+/// for (int i = 0; i < values.length; i++) {
+/// print('The value at index $i is now ${values[i]}');
+/// }
+/// });
+///
+/// obj['a'] = 10; // print will be triggered async
+///
+class CompoundObserver extends _Observer implements Bindable {
+ _ObservedSet _directObserver;
+ bool _reportChangesOnOpen;
+ List _observed = [];
+
+ CompoundObserver([this._reportChangesOnOpen = false]) {
+ _value = [];
+ }
+
+ int get _reportArgumentCount => 3;
+
+ /// Initiates observation and returns the initial value.
+ /// The callback will be passed the updated [value], and may optionally be
+ /// declared to take a second argument, which will contain the previous value.
+ ///
+ /// Implementation note: a third argument can also be declared, which will
+ /// receive a list of objects and paths, such that `list[2 * i]` will access
+ /// the object and `list[2 * i + 1]` will access the path, where `i` is the
+ /// order of the [addPath] call. This parameter is only used by
+ /// `package:polymer` as a performance optimization, and should not be relied
+ /// on in new code.
+ open(callback) => super.open(callback);
+
+ void _connect() {
+ for (var i = 0; i < _observed.length; i += 2) {
+ var object = _observed[i];
+ if (!identical(object, _observerSentinel)) {
+ _directObserver = new _ObservedSet(this, object);
+ break;
+ }
+ }
+
+ _check(skipChanges: !_reportChangesOnOpen);
+ }
+
+ void _disconnect() {
+ for (var i = 0; i < _observed.length; i += 2) {
+ if (identical(_observed[i], _observerSentinel)) {
+ _observed[i + 1].close();
+ }
+ }
+
+ _observed = null;
+ _value = null;
+
+ if (_directObserver != null) {
+ _directObserver.close(this);
+ _directObserver = null;
+ }
+ }
+
+ /// Adds a dependency on the property [path] accessed from [object].
+ /// [path] can be a [PropertyPath] or a [String]. If it is omitted an empty
+ /// path will be used.
+ void addPath(Object object, [path]) {
+ if (_isOpen || _isClosed) {
+ throw new StateError('Cannot add paths once started.');
+ }
+
+ path = new PropertyPath(path);
+ _observed..add(object)..add(path);
+ if (!_reportChangesOnOpen) return;
+ _value.add(path.getValueFrom(object));
+ }
+
+ void addObserver(Bindable observer) {
+ if (_isOpen || _isClosed) {
+ throw new StateError('Cannot add observers once started.');
+ }
+
+ _observed..add(_observerSentinel)..add(observer);
+ if (!_reportChangesOnOpen) return;
+ _value.add(observer.open((_) => deliver()));
+ }
+
+ void _iterateObjects(void observe(obj, prop)) {
+ for (var i = 0; i < _observed.length; i += 2) {
+ var object = _observed[i];
+ if (!identical(object, _observerSentinel)) {
+ (_observed[i + 1] as PropertyPath)._iterateObjects(object, observe);
+ }
+ }
+ }
+
+ bool _check({bool skipChanges: false}) {
+ bool changed = false;
+ _value.length = _observed.length ~/ 2;
+ var oldValues = null;
+ for (var i = 0; i < _observed.length; i += 2) {
+ var object = _observed[i];
+ var path = _observed[i + 1];
+ var value;
+ if (identical(object, _observerSentinel)) {
+ var observable = path as Bindable;
+ value = _state == _Observer._UNOPENED
+ ? observable.open((_) => this.deliver())
+ : observable.value;
+ } else {
+ value = (path as PropertyPath).getValueFrom(object);
+ }
+
+ if (skipChanges) {
+ _value[i ~/ 2] = value;
+ continue;
+ }
+
+ if (value == _value[i ~/ 2]) continue;
+
+ // don't allocate this unless necessary.
+ if (_notifyArgumentCount >= 2) {
+ if (oldValues == null) oldValues = new Map();
+ oldValues[i ~/ 2] = _value[i ~/ 2];
+ }
+
+ changed = true;
+ _value[i ~/ 2] = value;
+ }
+
+ if (!changed) return false;
+
+ // TODO(rafaelw): Having _observed as the third callback arg here is
+ // pretty lame API. Fix.
+ _report(_value, oldValues, _observed);
+ return true;
+ }
+}
+
+/// An object accepted by [PropertyPath] where properties are read and written
+/// as indexing operations, just like a [Map].
+abstract class Indexable<K, V> {
+ V operator [](K key);
+ operator []=(K key, V value);
+}
+
+const _observerSentinel = const _ObserverSentinel();
+
+class _ObserverSentinel {
+ const _ObserverSentinel();
+}
+
+// Visible for testing
+get observerSentinelForTesting => _observerSentinel;
+
+// A base class for the shared API implemented by PathObserver and
+// CompoundObserver and used in _ObservedSet.
+abstract class _Observer extends Bindable {
+ Function _notifyCallback;
+ int _notifyArgumentCount;
+ var _value;
+
+ // abstract members
+ void _iterateObjects(void observe(obj, prop));
+ void _connect();
+ void _disconnect();
+ bool _check({bool skipChanges: false});
+
+ static int _UNOPENED = 0;
+ static int _OPENED = 1;
+ static int _CLOSED = 2;
+ int _state = _UNOPENED;
+ bool get _isOpen => _state == _OPENED;
+ bool get _isClosed => _state == _CLOSED;
+
+ /// The number of arguments the subclass will pass to [_report].
+ int get _reportArgumentCount;
+
+ open(callback) {
+ if (_isOpen || _isClosed) {
+ throw new StateError('Observer has already been opened.');
+ }
+
+ if (smoke.minArgs(callback) > _reportArgumentCount) {
+ throw new ArgumentError('callback should take $_reportArgumentCount or '
+ 'fewer arguments');
+ }
+
+ _notifyCallback = callback;
+ _notifyArgumentCount = min(_reportArgumentCount, smoke.maxArgs(callback));
+
+ _connect();
+ _state = _OPENED;
+ return _value;
+ }
+
+ get value => _discardChanges();
+
+ void close() {
+ if (!_isOpen) return;
+
+ _disconnect();
+ _value = null;
+ _notifyCallback = null;
+ _state = _CLOSED;
+ }
+
+ _discardChanges() {
+ _check(skipChanges: true);
+ return _value;
+ }
+
+ void deliver() {
+ if (_isOpen) _dirtyCheck();
+ }
+
+ bool _dirtyCheck() {
+ var cycles = 0;
+ while (cycles < _MAX_DIRTY_CHECK_CYCLES && _check()) {
+ cycles++;
+ }
+ return cycles > 0;
+ }
+
+ void _report(newValue, oldValue, [extraArg]) {
+ try {
+ switch (_notifyArgumentCount) {
+ case 0:
+ _notifyCallback();
+ break;
+ case 1:
+ _notifyCallback(newValue);
+ break;
+ case 2:
+ _notifyCallback(newValue, oldValue);
+ break;
+ case 3:
+ _notifyCallback(newValue, oldValue, extraArg);
+ break;
+ }
+ } catch (e, s) {
+ // Deliver errors async, so if a single callback fails it doesn't prevent
+ // other things from working.
+ new Completer().completeError(e, s);
+ }
+ }
+}
+
+/// The observedSet abstraction is a perf optimization which reduces the total
+/// number of Object.observe observations of a set of objects. The idea is that
+/// groups of Observers will have some object dependencies in common and this
+/// observed set ensures that each object in the transitive closure of
+/// dependencies is only observed once. The observedSet acts as a write barrier
+/// such that whenever any change comes through, all Observers are checked for
+/// changed values.
+///
+/// Note that this optimization is explicitly moving work from setup-time to
+/// change-time.
+///
+/// TODO(rafaelw): Implement "garbage collection". In order to move work off
+/// the critical path, when Observers are closed, their observed objects are
+/// not Object.unobserve(d). As a result, it's possible that if the observedSet
+/// is kept open, but some Observers have been closed, it could cause "leaks"
+/// (prevent otherwise collectable objects from being collected). At some
+/// point, we should implement incremental "gc" which keeps a list of
+/// observedSets which may need clean-up and does small amounts of cleanup on a
+/// timeout until all is clean.
+class _ObservedSet {
+ /// To prevent sequential [PathObserver]s and [CompoundObserver]s from
+ /// observing the same object, we check if they are observing the same root
+ /// as the most recently created observer, and if so merge it into the
+ /// existing _ObservedSet.
+ ///
+ /// See <https://github.com/Polymer/observe-js/commit/f0990b1> and
+ /// <https://codereview.appspot.com/46780044/>.
+ static _ObservedSet _lastSet;
+
+ /// The root object for a [PathObserver]. For a [CompoundObserver], the root
+ /// object of the first path observed. This is used by the constructor to
+ /// reuse an [_ObservedSet] that starts from the same object.
+ Object _rootObject;
+
+ /// Subset of properties in [_rootObject] that we care about.
+ Set _rootObjectProperties;
+
+ /// Observers associated with this root object, in birth order.
+ final List<_Observer> _observers = [];
+
+ // Dart note: the JS implementation is O(N^2) because Array.indexOf is used
+ // for lookup in this array. We use HashMap to avoid this problem. It
+ // also gives us a nice way of tracking the StreamSubscription.
+ Map<Object, StreamSubscription> _objects;
+
+ factory _ObservedSet(_Observer observer, Object rootObject) {
+ if (_lastSet == null || !identical(_lastSet._rootObject, rootObject)) {
+ _lastSet = new _ObservedSet._(rootObject);
+ }
+ _lastSet.open(observer, rootObject);
+ return _lastSet;
+ }
+
+ _ObservedSet._(rootObject)
+ : _rootObject = rootObject,
+ _rootObjectProperties = rootObject == null ? null : new Set();
+
+ void open(_Observer obs, Object rootObject) {
+ if (_rootObject == null) {
+ _rootObject = rootObject;
+ _rootObjectProperties = new Set();
+ }
+
+ _observers.add(obs);
+ obs._iterateObjects(observe);
+ }
+
+ void close(_Observer obs) {
+ _observers.remove(obs);
+ if (_observers.isNotEmpty) return;
+
+ if (_objects != null) {
+ for (var sub in _objects.values) sub.cancel();
+ _objects = null;
+ }
+ _rootObject = null;
+ _rootObjectProperties = null;
+ if (identical(_lastSet, this)) _lastSet = null;
+ }
+
+ /// Observe now takes a second argument to indicate which property of an
+ /// object is being observed, so we don't trigger change notifications on
+ /// changes to unrelated properties.
+ void observe(Object obj, Object prop) {
+ if (identical(obj, _rootObject)) _rootObjectProperties.add(prop);
+ if (obj is ObservableList) _observeStream(obj.listChanges);
+ if (obj is Observable) _observeStream(obj.changes);
+ }
+
+ void _observeStream(Stream stream) {
+ // TODO(jmesserly): we hash on streams as we have two separate change
+ // streams for ObservableList. Not sure if that is the design we will use
+ // going forward.
+
+ if (_objects == null) _objects = new HashMap();
+ if (!_objects.containsKey(stream)) {
+ _objects[stream] = stream.listen(_callback);
+ }
+ }
+
+ /// Whether we can ignore all change events in [records]. This is true if all
+ /// records are for properties in the [_rootObject] and we are not observing
+ /// any of those properties. Changes on objects other than [_rootObject], or
+ /// changes for properties in [_rootObjectProperties] can't be ignored.
+ // Dart note: renamed from `allRootObjNonObservedProps` in the JS code.
+ bool _canIgnoreRecords(List<ChangeRecord> records) {
+ for (var rec in records) {
+ if (rec is PropertyChangeRecord) {
+ if (!identical(rec.object, _rootObject) ||
+ _rootObjectProperties.contains(rec.name)) {
+ return false;
+ }
+ } else if (rec is ListChangeRecord) {
+ if (!identical(rec.object, _rootObject) ||
+ _rootObjectProperties.contains(rec.index)) {
+ return false;
+ }
+ } else {
+ // TODO(sigmund): consider adding object to MapChangeRecord, and make
+ // this more precise.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ void _callback(List<ChangeRecord> records) {
+ if (_canIgnoreRecords(records)) return;
+ for (var observer in _observers.toList(growable: false)) {
+ if (observer._isOpen) observer._iterateObjects(observe);
+ }
+
+ for (var observer in _observers.toList(growable: false)) {
+ if (observer._isOpen) observer._check();
+ }
+ }
+}
+
+const int _MAX_DIRTY_CHECK_CYCLES = 1000;
diff --git a/pub/observe/lib/transformer.dart b/pub/observe/lib/transformer.dart
new file mode 100644
index 0000000..a7008bd
--- /dev/null
+++ b/pub/observe/lib/transformer.dart
@@ -0,0 +1,419 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Code transform for @observable. The core transformation is relatively
+/// straightforward, and essentially like an editor refactoring.
+@Deprecated('Parts of Observe used to support Polymer will move out of library')
+library observe.transformer;
+
+import 'dart:async';
+
+import 'package:analyzer/analyzer.dart';
+import 'package:analyzer/src/generated/ast.dart';
+import 'package:analyzer/src/generated/scanner.dart';
+import 'package:barback/barback.dart';
+import 'package:code_transformers/messages/build_logger.dart';
+import 'package:source_maps/refactor.dart';
+import 'package:source_span/source_span.dart';
+
+import 'src/messages.dart';
+
+/// A [Transformer] that replaces observables based on dirty-checking with an
+/// implementation based on change notifications.
+///
+/// The transformation adds hooks for field setters and notifies the observation
+/// system of the change.
+class ObservableTransformer extends Transformer {
+ final bool releaseMode;
+ final bool injectBuildLogsInOutput;
+ final List<String> _files;
+
+ ObservableTransformer(
+ {List<String> files, bool releaseMode, bool injectBuildLogsInOutput})
+ : _files = files,
+ releaseMode = releaseMode == true,
+ injectBuildLogsInOutput = injectBuildLogsInOutput == null
+ ? releaseMode != true
+ : injectBuildLogsInOutput;
+
+ ObservableTransformer.asPlugin(BarbackSettings settings)
+ : _files = _readFiles(settings.configuration['files']),
+ releaseMode = settings.mode == BarbackMode.RELEASE,
+ injectBuildLogsInOutput = settings.mode != BarbackMode.RELEASE;
+
+ static List<String> _readFiles(value) {
+ if (value == null) return null;
+ var files = <String>[];
+ bool error;
+ if (value is List) {
+ files = new List<String>.from(value);
+ error = value.any((e) => e is! String);
+ } else if (value is String) {
+ files = [value];
+ error = false;
+ } else {
+ error = true;
+ }
+ if (error) print('Invalid value for "files" in the observe transformer.');
+ return files;
+ }
+
+ // TODO(nweiz): This should just take an AssetId when barback <0.13.0 support
+ // is dropped.
+ Future<bool> isPrimary(Object idOrAsset) {
+ var id = idOrAsset is AssetId ? idOrAsset : (idOrAsset as Asset).id;
+ return new Future.value(id.extension == '.dart' &&
+ (_files == null || _files.contains(id.path)));
+ }
+
+ Future apply(Transform transform) {
+ return transform.primaryInput.readAsString().then((content) {
+ // Do a quick string check to determine if this is this file even
+ // plausibly might need to be transformed. If not, we can avoid an
+ // expensive parse.
+ if (!observableMatcher.hasMatch(content)) return null;
+
+ var id = transform.primaryInput.id;
+ // TODO(sigmund): improve how we compute this url
+ var url = id.path.startsWith('lib/')
+ ? 'package:${id.package}/${id.path.substring(4)}'
+ : id.path;
+ var sourceFile = new SourceFile(content, url: url);
+ var logger = new BuildLogger(transform,
+ convertErrorsToWarnings: !releaseMode,
+ detailsUri: 'http://goo.gl/5HPeuP');
+ var transaction = _transformCompilationUnit(content, sourceFile, logger);
+ if (!transaction.hasEdits) {
+ transform.addOutput(transform.primaryInput);
+ } else {
+ var printer = transaction.commit();
+ // TODO(sigmund): emit source maps when barback supports it (see
+ // dartbug.com/12340)
+ printer.build(url);
+ transform.addOutput(new Asset.fromString(id, printer.text));
+ }
+
+ if (injectBuildLogsInOutput) return logger.writeOutput();
+ });
+ }
+}
+
+TextEditTransaction _transformCompilationUnit(
+ String inputCode, SourceFile sourceFile, BuildLogger logger) {
+ var unit = parseCompilationUnit(inputCode, suppressErrors: true);
+ var code = new TextEditTransaction(inputCode, sourceFile);
+ for (var directive in unit.directives) {
+ if (directive is LibraryDirective && _hasObservable(directive)) {
+ logger.warning(NO_OBSERVABLE_ON_LIBRARY,
+ span: _getSpan(sourceFile, directive));
+ break;
+ }
+ }
+
+ for (var declaration in unit.declarations) {
+ if (declaration is ClassDeclaration) {
+ _transformClass(declaration, code, sourceFile, logger);
+ } else if (declaration is TopLevelVariableDeclaration) {
+ if (_hasObservable(declaration)) {
+ logger.warning(NO_OBSERVABLE_ON_TOP_LEVEL,
+ span: _getSpan(sourceFile, declaration));
+ }
+ }
+ }
+ return code;
+}
+
+_getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end);
+
+/// True if the node has the `@observable` or `@published` annotation.
+// TODO(jmesserly): it is not good to be hard coding Polymer support here.
+bool _hasObservable(AnnotatedNode node) =>
+ node.metadata.any(_isObservableAnnotation);
+
+// TODO(jmesserly): this isn't correct if the annotation has been imported
+// with a prefix, or cases like that. We should technically be resolving, but
+// that is expensive in analyzer, so it isn't feasible yet.
+bool _isObservableAnnotation(Annotation node) =>
+ _isAnnotationContant(node, 'observable') ||
+ _isAnnotationContant(node, 'published') ||
+ _isAnnotationType(node, 'ObservableProperty') ||
+ _isAnnotationType(node, 'PublishedProperty');
+
+bool _isAnnotationContant(Annotation m, String name) =>
+ m.name.name == name && m.constructorName == null && m.arguments == null;
+
+bool _isAnnotationType(Annotation m, String name) => m.name.name == name;
+
+void _transformClass(ClassDeclaration cls, TextEditTransaction code,
+ SourceFile file, BuildLogger logger) {
+ if (_hasObservable(cls)) {
+ logger.warning(NO_OBSERVABLE_ON_CLASS, span: _getSpan(file, cls));
+ }
+
+ // We'd like to track whether observable was declared explicitly, otherwise
+ // report a warning later below. Because we don't have type analysis (only
+ // syntactic understanding of the code), we only report warnings that are
+ // known to be true.
+ var explicitObservable = false;
+ var implicitObservable = false;
+ if (cls.extendsClause != null) {
+ var id = _getSimpleIdentifier(cls.extendsClause.superclass.name);
+ if (id.name == 'AutoObservable') {
+ code.edit(id.offset, id.end, 'Observable');
+ explicitObservable = true;
+ } else if (id.name == 'Observable') {
+ explicitObservable = true;
+ } else if (id.name != 'HtmlElement' &&
+ id.name != 'CustomElement' &&
+ id.name != 'Object') {
+ // TODO(sigmund): this is conservative, consider using type-resolution to
+ // improve this check.
+ implicitObservable = true;
+ }
+ }
+
+ if (cls.withClause != null) {
+ for (var type in cls.withClause.mixinTypes) {
+ var id = _getSimpleIdentifier(type.name);
+ if (id.name == 'AutoObservable') {
+ code.edit(id.offset, id.end, 'Observable');
+ explicitObservable = true;
+ break;
+ } else if (id.name == 'Observable') {
+ explicitObservable = true;
+ break;
+ } else {
+ // TODO(sigmund): this is conservative, consider using type-resolution
+ // to improve this check.
+ implicitObservable = true;
+ }
+ }
+ }
+
+ if (cls.implementsClause != null) {
+ // TODO(sigmund): consider adding type-resolution to give a more precise
+ // answer.
+ implicitObservable = true;
+ }
+
+ var declaresObservable = explicitObservable || implicitObservable;
+
+ // Track fields that were transformed.
+ var instanceFields = new Set<String>();
+
+ for (var member in cls.members) {
+ if (member is FieldDeclaration) {
+ if (member.isStatic) {
+ if (_hasObservable(member)) {
+ logger.warning(NO_OBSERVABLE_ON_STATIC_FIELD,
+ span: _getSpan(file, member));
+ }
+ continue;
+ }
+ if (_hasObservable(member)) {
+ if (!declaresObservable) {
+ logger.warning(REQUIRE_OBSERVABLE_INTERFACE,
+ span: _getSpan(file, member));
+ }
+ _transformFields(file, member, code, logger);
+
+ var names = member.fields.variables.map((v) => v.name.name);
+
+ if (!_isReadOnly(member.fields)) instanceFields.addAll(names);
+ }
+ }
+ }
+
+ // If nothing was @observable, bail.
+ if (instanceFields.length == 0) return;
+
+ if (!explicitObservable) _mixinObservable(cls, code);
+
+ // Fix initializers, because they aren't allowed to call the setter.
+ for (var member in cls.members) {
+ if (member is ConstructorDeclaration) {
+ _fixConstructor(member, code, instanceFields);
+ }
+ }
+}
+
+/// Adds "with Observable" and associated implementation.
+void _mixinObservable(ClassDeclaration cls, TextEditTransaction code) {
+ // Note: we need to be careful to put the with clause after extends, but
+ // before implements clause.
+ if (cls.withClause != null) {
+ var pos = cls.withClause.end;
+ code.edit(pos, pos, ', Observable');
+ } else if (cls.extendsClause != null) {
+ var pos = cls.extendsClause.end;
+ code.edit(pos, pos, ' with Observable ');
+ } else {
+ var params = cls.typeParameters;
+ var pos = params != null ? params.end : cls.name.end;
+ code.edit(pos, pos, ' extends Observable ');
+ }
+}
+
+SimpleIdentifier _getSimpleIdentifier(Identifier id) =>
+ id is PrefixedIdentifier ? id.identifier : id;
+
+bool _hasKeyword(Token token, Keyword keyword) =>
+ token?.type == TokenType.KEYWORD && token.lexeme == keyword.syntax;
+
+String _getOriginalCode(TextEditTransaction code, AstNode node) =>
+ code.original.substring(node.offset, node.end);
+
+void _fixConstructor(ConstructorDeclaration ctor, TextEditTransaction code,
+ Set<String> changedFields) {
+ // Fix normal initializers
+ for (var initializer in ctor.initializers) {
+ if (initializer is ConstructorFieldInitializer) {
+ var field = initializer.fieldName;
+ if (changedFields.contains(field.name)) {
+ code.edit(field.offset, field.end, '__\$${field.name}');
+ }
+ }
+ }
+
+ // Fix "this." initializer in parameter list. These are tricky:
+ // we need to preserve the name and add an initializer.
+ // Preserving the name is important for named args, and for dartdoc.
+ // BEFORE: Foo(this.bar, this.baz) { ... }
+ // AFTER: Foo(bar, baz) : __$bar = bar, __$baz = baz { ... }
+
+ var thisInit = [];
+ for (var param in ctor.parameters.parameters) {
+ if (param is DefaultFormalParameter) {
+ param = (param as DefaultFormalParameter).parameter;
+ }
+ if (param is FieldFormalParameter) {
+ var name = param.identifier.name;
+ if (changedFields.contains(name)) {
+ thisInit.add(name);
+ // Remove "this." but keep everything else.
+ code.edit(param.thisKeyword.offset, param.period.end, '');
+ }
+ }
+ }
+
+ if (thisInit.length == 0) return;
+
+ // TODO(jmesserly): smarter formatting with indent, etc.
+ var inserted = thisInit.map((i) => '__\$$i = $i').join(', ');
+
+ int offset;
+ if (ctor.separator != null) {
+ offset = ctor.separator.end;
+ inserted = ' $inserted,';
+ } else {
+ offset = ctor.parameters.end;
+ inserted = ' : $inserted';
+ }
+
+ code.edit(offset, offset, inserted);
+}
+
+bool _isReadOnly(VariableDeclarationList fields) {
+ return _hasKeyword(fields.keyword, Keyword.CONST) ||
+ _hasKeyword(fields.keyword, Keyword.FINAL);
+}
+
+void _transformFields(SourceFile file, FieldDeclaration member,
+ TextEditTransaction code, BuildLogger logger) {
+ final fields = member.fields;
+ if (_isReadOnly(fields)) return;
+
+ // Private fields aren't supported:
+ for (var field in fields.variables) {
+ final name = field.name.name;
+ if (Identifier.isPrivateName(name)) {
+ logger.warning('Cannot make private field $name observable.',
+ span: _getSpan(file, field));
+ return;
+ }
+ }
+
+ // Unfortunately "var" doesn't work in all positions where type annotations
+ // are allowed, such as "var get name". So we use "dynamic" instead.
+ var type = 'dynamic';
+ if (fields.type != null) {
+ type = _getOriginalCode(code, fields.type);
+ } else if (_hasKeyword(fields.keyword, Keyword.VAR)) {
+ // Replace 'var' with 'dynamic'
+ code.edit(fields.keyword.offset, fields.keyword.end, type);
+ }
+
+ // Note: the replacements here are a bit subtle. It needs to support multiple
+ // fields declared via the same @observable, as well as preserving newlines.
+ // (Preserving newlines is important because it allows the generated code to
+ // be debugged without needing a source map.)
+ //
+ // For example:
+ //
+ // @observable
+ // @otherMetaData
+ // Foo
+ // foo = 1, bar = 2,
+ // baz;
+ //
+ // Will be transformed into something like:
+ //
+ // @reflectable @observable
+ // @OtherMetaData()
+ // Foo
+ // get foo => __foo; Foo __foo = 1; @reflectable set foo ...; ...
+ // @observable @OtherMetaData() Foo get baz => __baz; Foo baz; ...
+ //
+ // Metadata is moved to the getter.
+
+ String metadata = '';
+ if (fields.variables.length > 1) {
+ metadata = member.metadata.map((m) => _getOriginalCode(code, m)).join(' ');
+ metadata = '@reflectable $metadata';
+ }
+
+ for (int i = 0; i < fields.variables.length; i++) {
+ final field = fields.variables[i];
+ final name = field.name.name;
+
+ var beforeInit = 'get $name => __\$$name; $type __\$$name';
+
+ // The first field is expanded differently from subsequent fields, because
+ // we can reuse the metadata and type annotation.
+ if (i == 0) {
+ final begin = member.metadata.first.offset;
+ code.edit(begin, begin, '@reflectable ');
+ } else {
+ beforeInit = '$metadata $type $beforeInit';
+ }
+
+ code.edit(field.name.offset, field.name.end, beforeInit);
+
+ // Replace comma with semicolon
+ final end = _findFieldSeperator(field.endToken.next);
+ if (end.type == TokenType.COMMA) code.edit(end.offset, end.end, ';');
+
+ code.edit(
+ end.end,
+ end.end,
+ ' @reflectable set $name($type value) { '
+ '__\$$name = notifyPropertyChange(#$name, __\$$name, value); }');
+ }
+}
+
+Token _findFieldSeperator(Token token) {
+ while (token != null) {
+ if (token.type == TokenType.COMMA || token.type == TokenType.SEMICOLON) {
+ break;
+ }
+ token = token.next;
+ }
+ return token;
+}
+
+// TODO(sigmund): remove hard coded Polymer support (@published). The proper way
+// to do this would be to switch to use the analyzer to resolve whether
+// annotations are subtypes of ObservableProperty.
+final observableMatcher =
+ new RegExp("@(published|observable|PublishedProperty|ObservableProperty)");
diff --git a/pub/observe/pubspec.yaml b/pub/observe/pubspec.yaml
new file mode 100644
index 0000000..e9a4b36
--- /dev/null
+++ b/pub/observe/pubspec.yaml
@@ -0,0 +1,37 @@
+name: observe
+version: 0.15.0
+author: Polymer.dart Authors <web-ui-dev@dartlang.org>
+description: >
+ Observable properties and objects for use in template_binding.
+ Template Binding extends HTML and the DOM APIs to support a sensible
+ separation between the UI (DOM) of a document or application and its
+ underlying data (model). Updates to the model are reflected in the DOM and
+ user input into the DOM is immediately assigned to the model.
+homepage: https://www.dartlang.org/polymer-dart/
+dependencies:
+ analyzer: ^0.27.0
+ barback: '>=0.14.2 <0.16.0'
+ func: ^0.1.0
+ logging: '>=0.9.0 <0.12.0'
+ observable: '^0.17.0'
+ path: '>=0.9.0 <2.0.0'
+ smoke: '>=0.1.0 <0.4.0'
+ source_maps: '>=0.9.4 <0.11.0'
+ source_span: ^1.0.0
+ utf: ^0.9.0
+ code_transformers: ^0.4.2
+dev_dependencies:
+ benchmark_harness: '>=1.0.0 <2.0.0'
+ browser: any
+ chart: '>=1.0.8 <2.0.0'
+ test: '^0.12.0'
+ stack_trace: '>=0.9.1 <2.0.0'
+environment:
+ sdk: ">=1.9.0 <2.0.0"
+transformers:
+- observe:
+ files:
+ - benchmark/test_observable.dart
+ - benchmark/test_path_observable.dart
+- test/pub_serve:
+ $include: test/**_test.dart
diff --git a/pub/platform/.analysis_options b/pub/platform/.analysis_options
old mode 100644
new mode 100755
diff --git a/pub/platform/.gitignore b/pub/platform/.gitignore
old mode 100644
new mode 100755
diff --git a/pub/platform/.travis.yml b/pub/platform/.travis.yml
old mode 100644
new mode 100755
diff --git a/pub/platform/AUTHORS b/pub/platform/AUTHORS
old mode 100644
new mode 100755
diff --git a/pub/platform/BUILD.gn b/pub/platform/BUILD.gn
index 3062c4f..03587fc 100644
--- a/pub/platform/BUILD.gn
+++ b/pub/platform/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for platform-1.1.1
+# This file is generated by importer.py for platform-2.0.0
import("//build/dart/dart_package.gni")
diff --git a/pub/platform/CHANGELOG.md b/pub/platform/CHANGELOG.md
old mode 100644
new mode 100755
index 8ec72a7..25d0ae2
--- a/pub/platform/CHANGELOG.md
+++ b/pub/platform/CHANGELOG.md
@@ -1,3 +1,7 @@
+### 2.0.0
+* Added `stdinSupportsAnsi` and `stdinSupportsAnsi`
+* Removed `ansiSupported`
+
### 1.1.1
* Updated `LocalPlatform` to use new `dart.io` API for ansi color support queries
diff --git a/pub/platform/CONTRIBUTING.md b/pub/platform/CONTRIBUTING.md
old mode 100644
new mode 100755
diff --git a/pub/platform/LICENSE b/pub/platform/LICENSE
old mode 100644
new mode 100755
diff --git a/pub/platform/README.md b/pub/platform/README.md
old mode 100644
new mode 100755
index 00f0773..0487c5c
--- a/pub/platform/README.md
+++ b/pub/platform/README.md
@@ -2,6 +2,7 @@
[![Build Status -](https://travis-ci.org/google/platform.dart.svg?branch=master)](https://travis-ci.org/google/platform.dart)
[![Coverage Status -](https://coveralls.io/repos/github/google/platform.dart/badge.svg?branch=master)](https://coveralls.io/github/google/platform.dart?branch=master)
+[![Pub](https://img.shields.io/pub/v/platform.svg)](https://pub.dartlang.org/packages/platform)
A generic platform abstraction for Dart.
diff --git a/pub/platform/lib/platform.dart b/pub/platform/lib/platform.dart
old mode 100644
new mode 100755
diff --git a/pub/platform/lib/src/interface/local_platform.dart b/pub/platform/lib/src/interface/local_platform.dart
old mode 100644
new mode 100755
index e9e7bfb..c90256a
--- a/pub/platform/lib/src/interface/local_platform.dart
+++ b/pub/platform/lib/src/interface/local_platform.dart
@@ -2,7 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-import 'dart:io' as io show Platform, stdin;
+import 'dart:io' as io show Platform, stdin, stdout;
import 'platform.dart';
@@ -48,5 +48,8 @@
String get version => io.Platform.version;
@override
- bool get ansiSupported => io.stdin.supportsAnsiEscapes;
+ bool get stdinSupportsAnsi => io.stdin.supportsAnsiEscapes;
+
+ @override
+ bool get stdoutSupportsAnsi => io.stdout.supportsAnsiEscapes;
}
diff --git a/pub/platform/lib/src/interface/platform.dart b/pub/platform/lib/src/interface/platform.dart
old mode 100644
new mode 100755
index a603e3c..c750108
--- a/pub/platform/lib/src/interface/platform.dart
+++ b/pub/platform/lib/src/interface/platform.dart
@@ -114,11 +114,11 @@
/// whitespace and other version and build details.
String get version;
- /// When stdio is connected to a terminal, whether ANSI codes are supported.
- ///
- /// This value is hard-coded to true, except on Windows where only more recent
- /// versions of Windows 10 support the codes.
- bool get ansiSupported;
+ /// When stdin is connected to a terminal, whether ANSI codes are supported.
+ bool get stdinSupportsAnsi;
+
+ /// When stdout is connected to a terminal, whether ANSI codes are supported.
+ bool get stdoutSupportsAnsi;
/// Returns a JSON-encoded representation of this platform.
String toJson() {
@@ -135,7 +135,8 @@
'packageRoot': packageRoot,
'packageConfig': packageConfig,
'version': version,
- 'ansiSupported': ansiSupported,
+ 'stdinSupportsAnsi': stdinSupportsAnsi,
+ 'stdoutSupportsAnsi': stdoutSupportsAnsi,
});
}
}
diff --git a/pub/platform/lib/src/testing/fake_platform.dart b/pub/platform/lib/src/testing/fake_platform.dart
old mode 100644
new mode 100755
index ceba4cd..16b8092
--- a/pub/platform/lib/src/testing/fake_platform.dart
+++ b/pub/platform/lib/src/testing/fake_platform.dart
@@ -25,7 +25,8 @@
this.packageRoot,
this.packageConfig,
this.version,
- this.ansiSupported,
+ this.stdinSupportsAnsi,
+ this.stdoutSupportsAnsi,
});
/// Creates a new [FakePlatform] with properties whose initial values mirror
@@ -44,7 +45,8 @@
packageRoot = platform.packageRoot,
packageConfig = platform.packageConfig,
version = platform.version,
- ansiSupported = platform.ansiSupported;
+ stdinSupportsAnsi = platform.stdinSupportsAnsi,
+ stdoutSupportsAnsi = platform.stdoutSupportsAnsi;
/// Creates a new [FakePlatform] with properties extracted from the encoded
/// JSON string.
@@ -66,7 +68,8 @@
packageRoot: map['packageRoot'],
packageConfig: map['packageConfig'],
version: map['version'],
- ansiSupported: map['ansiSupported'],
+ stdinSupportsAnsi: map['stdinSupportsAnsi'],
+ stdoutSupportsAnsi: map['stdoutSupportsAnsi'],
);
}
@@ -107,5 +110,8 @@
String version;
@override
- bool ansiSupported;
+ bool stdinSupportsAnsi;
+
+ @override
+ bool stdoutSupportsAnsi;
}
diff --git a/pub/platform/pubspec.yaml b/pub/platform/pubspec.yaml
old mode 100644
new mode 100755
index 74f5564..ab7c8e9
--- a/pub/platform/pubspec.yaml
+++ b/pub/platform/pubspec.yaml
@@ -1,5 +1,5 @@
name: platform
-version: 1.1.1
+version: 2.0.0
authors:
- Todd Volkert <tvolkert@google.com>
description: A pluggable, mockable platform abstraction for Dart.
diff --git a/pub/process/.analysis_options b/pub/process/.analysis_options
old mode 100644
new mode 100755
diff --git a/pub/process/.gitignore b/pub/process/.gitignore
old mode 100644
new mode 100755
diff --git a/pub/process/.travis.yml b/pub/process/.travis.yml
old mode 100644
new mode 100755
diff --git a/pub/process/AUTHORS b/pub/process/AUTHORS
old mode 100644
new mode 100755
diff --git a/pub/process/BUILD.gn b/pub/process/BUILD.gn
index 9949f05..2ec3241 100644
--- a/pub/process/BUILD.gn
+++ b/pub/process/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for process-2.0.1
+# This file is generated by importer.py for process-2.0.3
import("//build/dart/dart_package.gni")
diff --git a/pub/process/CHANGELOG.md b/pub/process/CHANGELOG.md
old mode 100644
new mode 100755
index cc43eb1..93d7fa2
--- a/pub/process/CHANGELOG.md
+++ b/pub/process/CHANGELOG.md
@@ -1,3 +1,11 @@
+### 2.0.3
+* relax dependency requirement for `platform`
+
+#### 2.0.2
+
+* Fix a strong mode function expression return type inference bug with Dart
+ 1.23.0-dev.10.0.
+
#### 2.0.1
* Fixed bug in `ReplayProcessManager` whereby it could try to write to `stdout`
diff --git a/pub/process/CONTRIBUTING.md b/pub/process/CONTRIBUTING.md
old mode 100644
new mode 100755
diff --git a/pub/process/LICENSE b/pub/process/LICENSE
old mode 100644
new mode 100755
diff --git a/pub/process/README.md b/pub/process/README.md
old mode 100644
new mode 100755
diff --git a/pub/process/lib/process.dart b/pub/process/lib/process.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/record_replay.dart b/pub/process/lib/record_replay.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/interface/common.dart b/pub/process/lib/src/interface/common.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/interface/local_process_manager.dart b/pub/process/lib/src/interface/local_process_manager.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/interface/process_manager.dart b/pub/process/lib/src/interface/process_manager.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/record_replay/can_run_manifest_entry.dart b/pub/process/lib/src/record_replay/can_run_manifest_entry.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/record_replay/command_element.dart b/pub/process/lib/src/record_replay/command_element.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/record_replay/common.dart b/pub/process/lib/src/record_replay/common.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/record_replay/constants.dart b/pub/process/lib/src/record_replay/constants.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/record_replay/manifest.dart b/pub/process/lib/src/record_replay/manifest.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/record_replay/manifest_entry.dart b/pub/process/lib/src/record_replay/manifest_entry.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/record_replay/recording_process_manager.dart b/pub/process/lib/src/record_replay/recording_process_manager.dart
old mode 100644
new mode 100755
index c1f337c..8b9cd92
--- a/pub/process/lib/src/record_replay/recording_process_manager.dart
+++ b/pub/process/lib/src/record_replay/recording_process_manager.dart
@@ -319,8 +319,9 @@
void callOnTimeout(int pid) => onTimeout(_manifest.getRunEntry(pid));
await Future
.wait(new List<Future<int>>.from(_runningProcesses.values))
- .timeout(timeout,
- onTimeout: () => _runningProcesses.keys.forEach(callOnTimeout));
+ .timeout(timeout, onTimeout: () {
+ _runningProcesses.keys.forEach(callOnTimeout);
+ });
}
/// Writes our process invocation manifest to disk in the destination folder.
diff --git a/pub/process/lib/src/record_replay/replay_process_manager.dart b/pub/process/lib/src/record_replay/replay_process_manager.dart
old mode 100644
new mode 100755
diff --git a/pub/process/lib/src/record_replay/run_manifest_entry.dart b/pub/process/lib/src/record_replay/run_manifest_entry.dart
old mode 100644
new mode 100755
diff --git a/pub/process/pubspec.yaml b/pub/process/pubspec.yaml
old mode 100644
new mode 100755
index 75e0544..d565ac4
--- a/pub/process/pubspec.yaml
+++ b/pub/process/pubspec.yaml
@@ -1,5 +1,5 @@
name: process
-version: 2.0.1
+version: 2.0.3
authors:
- Todd Volkert <tvolkert@google.com>
- Michael Goderbauer <goderbauer@google.com>
@@ -11,7 +11,7 @@
intl: '>=0.14.0 <0.15.0'
meta: ^1.0.4
path: ^1.4.0
- platform: ^1.0.1
+ platform: '>=1.0.1 <3.0.0'
dev_dependencies:
test: ^0.12.10
diff --git a/pub/smoke/.gitignore b/pub/smoke/.gitignore
new file mode 100644
index 0000000..b0f213a
--- /dev/null
+++ b/pub/smoke/.gitignore
@@ -0,0 +1,7 @@
+pubspec.lock
+.pub
+packages
+.idea
+/build/
+.packages
+.analysis_options
diff --git a/pub/smoke/.status b/pub/smoke/.status
new file mode 100644
index 0000000..9a4bb95
--- /dev/null
+++ b/pub/smoke/.status
@@ -0,0 +1,22 @@
+# Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+# Don't run any test-like files that show up in packages directories. It
+# shouldn't be necessary to run "pub install" in these packages, but if you do
+# it shouldn't break the tests.
+*/packages/*/*: Skip
+*/*/packages/*/*: Skip
+*/*/*/packages/*/*: Skip
+*/*/*/*/packages/*/*: Skip
+*/*/*/*/*/packages/*/*: Skip
+
+[ $runtime == vm && $mode == debug]
+test/codegen/end_to_end_test: Skip # Times out
+test/codegen/recorder_test: Skip # Times out
+
+[ $browser ]
+build/test/codegen/end_to_end_test: Skip # Uses dart:io.
+build/test/codegen/recorder_test: Skip # Uses dart:io.
+test/codegen/end_to_end_test: Skip # Uses dart:io.
+test/codegen/recorder_test: Skip # Uses dart:io.
diff --git a/pub/smoke/.test_config b/pub/smoke/.test_config
new file mode 100644
index 0000000..2535563
--- /dev/null
+++ b/pub/smoke/.test_config
@@ -0,0 +1,3 @@
+{
+ "test_package": true
+}
diff --git a/pub/smoke/AUTHORS b/pub/smoke/AUTHORS
new file mode 100644
index 0000000..0617765
--- /dev/null
+++ b/pub/smoke/AUTHORS
@@ -0,0 +1,9 @@
+# Names should be added to this file with this pattern:
+#
+# For individuals:
+# Name <email address>
+#
+# For organizations:
+# Organization <fnmatch pattern>
+#
+Google Inc. <*@google.com>
diff --git a/pub/smoke/BUILD.gn b/pub/smoke/BUILD.gn
new file mode 100644
index 0000000..a1dbf90
--- /dev/null
+++ b/pub/smoke/BUILD.gn
@@ -0,0 +1,17 @@
+# This file is generated by importer.py for smoke-0.3.6+2
+
+import("//build/dart/dart_package.gni")
+
+dart_package("smoke") {
+ package_name = "smoke"
+
+ source_dir = "lib"
+
+ disable_analysis = true
+
+ deps = [
+ "//third_party/dart-pkg/pub/barback",
+ "//third_party/dart-pkg/pub/logging",
+ "//dart/pkg/analyzer",
+ ]
+}
diff --git a/pub/smoke/CHANGELOG.md b/pub/smoke/CHANGELOG.md
new file mode 100644
index 0000000..4e24e6b
--- /dev/null
+++ b/pub/smoke/CHANGELOG.md
@@ -0,0 +1,60 @@
+#### 0.3.6+2
+ * Eliminate errors reported by analyzer in strong mode.
+
+#### 0.3.6+1
+ * Update to use the `transformer_test` package instead of `code_transformers`
+ for tests.
+
+#### 0.3.6
+ * Update to analyzer '^0.27.0' and update to the test package.
+
+#### 0.3.5
+ * Update to analyzer '<0.27.0'
+
+#### 0.3.4
+ * Add excludeOverriden to QueryOptions which removes declarations that were
+ overriden within the class hierarchy.
+
+#### 0.3.3+1
+ * Update logging package to `<0.12.0`.
+
+#### 0.3.3
+ * Update to analyzer `<0.26.0`.
+
+#### 0.3.2
+ * Work around an issue running Dart analyzer on the generated code, if the
+ `dynamic` type appeared in the output. Smoke will now use `Object` instead.
+
+#### 0.3.1+1
+ * Updated dependency versions.
+
+#### 0.3.1
+ * Add canAcceptNArgs method.
+
+#### 0.3.0
+ * Change SUPPORTED_ARGS limit for minArgs and maxArgs method from 3 to 15.
+
+#### 0.2.1+1
+ * Fix toString calls on Type instances.
+
+#### 0.2.0+3
+ * Widen the constraint on analyzer.
+
+#### 0.2.0+2
+ * Widen the constraint on barback.
+
+#### 0.2.0+1
+ * Switch from `source_maps`' `Span` class to `source_span`'s `SourceSpan`
+ class.
+
+#### 0.2.0
+ * Static configuration can be modified, so code generators can split the
+ static configuration in pieces.
+ * **breaking change**: for codegen call `writeStaticConfiguration` instead of
+ `writeInitCall`.
+
+#### 0.1.0
+ * Initial release: introduces the smoke API, a mirror based implementation, a
+ statically configured implementation that can be declared by hand or be
+ generated by tools, and libraries that help generate the static
+ configurations.
diff --git a/pub/smoke/LICENSE b/pub/smoke/LICENSE
new file mode 100644
index 0000000..95987ba
--- /dev/null
+++ b/pub/smoke/LICENSE
@@ -0,0 +1,27 @@
+// Copyright (c) 2014 The Polymer Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pub/smoke/PATENTS b/pub/smoke/PATENTS
new file mode 100644
index 0000000..e120963
--- /dev/null
+++ b/pub/smoke/PATENTS
@@ -0,0 +1,23 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Polymer project.
+
+Google hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this section)
+patent license to make, have made, use, offer to sell, sell, import,
+transfer and otherwise run, modify and propagate the contents of this
+implementation of Polymer, where such license applies only to those
+patent claims, both currently owned or controlled by Google and acquired
+in the future, licensable by Google that are necessarily infringed by
+this implementation of Polymer. This grant does not include claims
+that would be infringed only as a consequence of further modification of
+this implementation. If you or your agent or exclusive licensee
+institute or order or agree to the institution of patent litigation
+against any entity (including a cross-claim or counterclaim in a
+lawsuit) alleging that this implementation of Polymer or any code
+incorporated within this implementation of Polymer constitutes
+direct or contributory patent infringement, or inducement of patent
+infringement, then any patent rights granted to you under this License
+for this implementation of Polymer shall terminate as of the date
+such litigation is filed.
diff --git a/pub/smoke/README.md b/pub/smoke/README.md
new file mode 100644
index 0000000..685e1c5
--- /dev/null
+++ b/pub/smoke/README.md
@@ -0,0 +1,40 @@
+Smoke (and mirrors)
+===================
+
+Smoke is a package that exposes a reduced reflective system API. This API
+includes accessing objects in a dynamic fashion (read properties, write
+properties, and call methods), inspecting types (for example, whether a
+method exists), and symbol/string convertion.
+
+The package provides a default implementation of this API that uses the system's
+mirrors, but additionally provides mechanisms for statically generating code
+that can replace the mirror-based implementation.
+
+The intention of this package is to allow frameworks to use mirrors in a way
+that will not impose on their users. The idea is that users will not worry about
+how to preserve symbols when compiling with dart2js (for instance, using the
+[MirrorsUsed][] annotation). Instead, this package provides the building
+blocks to autogenerate whatever is needed for dart2js to be happy and to
+generate reasonable code.
+
+Note this package alone doesn't know how to generate everything, but it provides
+a simple API that different frameworks can use to define what needs to be
+generated.
+
+
+Smoke reflective API
+====================
+
+Use `package:smoke/smoke.dart` in your framework to read and write objects and
+to inspect type information. Read the Dart-docs for more details.
+
+Code Generation
+===============
+
+Use `package:smoke/codegen/generator.dart` and
+`package:smoke/codegen/recorder.dart` in your transformer to create a static
+initialization that can be used by smoke. The test under
+`test/codegen/end_to_end_test.dart` is a good illustrating example to learn how
+to use these APIs.
+
+[MirrorsUsed]: https://api.dartlang.org/apidocs/channels/stable/#dart-mirrors.MirrorsUsed
diff --git a/pub/smoke/codereview.settings b/pub/smoke/codereview.settings
new file mode 100644
index 0000000..c24df00
--- /dev/null
+++ b/pub/smoke/codereview.settings
@@ -0,0 +1,3 @@
+CODE_REVIEW_SERVER: https://codereview.chromium.org/
+VIEW_VC: https://github.com/dart-lang/smoke/commit/
+CC_LIST: reviews@dartlang.org
\ No newline at end of file
diff --git a/pub/smoke/lib/codegen/generator.dart b/pub/smoke/lib/codegen/generator.dart
new file mode 100644
index 0000000..3a67687
--- /dev/null
+++ b/pub/smoke/lib/codegen/generator.dart
@@ -0,0 +1,522 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Library to generate code that can initialize the `StaticConfiguration` in
+/// `package:smoke/static.dart`.
+///
+/// This library doesn't have any specific logic to extract information from
+/// Dart source code. To extract code using the analyzer, take a look at the
+/// `smoke.codegen.recorder` library.
+library smoke.codegen.generator;
+
+import 'dart:collection' show SplayTreeMap, SplayTreeSet;
+
+import 'package:smoke/src/common.dart' show compareLists, compareMaps;
+
+/// Collects the necessary information and generates code to initialize a
+/// `StaticConfiguration`. After setting up the generator by calling
+/// [addGetter], [addSetter], [addSymbol], and [addDeclaration], you can
+/// retrieve the generated code using the following three methods:
+///
+/// * [writeImports] writes a list of imports directives,
+/// * [writeTopLevelDeclarations] writes additional declarations used to
+/// represent mixin classes by name in the generated code.
+/// * [writeStaticConfiguration] writes the actual code that allocates the
+/// static configuration.
+///
+/// You'd need to include all three in your generated code, since the
+/// initialization code refers to symbols that are only available from the
+/// generated imports or the generated top-level declarations.
+class SmokeCodeGenerator {
+ // Note: we use SplayTreeSet/Map here and below to keep generated code sorted.
+ /// Names used as getters via smoke.
+ final Set<String> _getters = new SplayTreeSet();
+
+ /// Names used as setters via smoke.
+ final Set<String> _setters = new SplayTreeSet();
+
+ /// Subclass relations needed to run smoke queries.
+ final Map<TypeIdentifier, TypeIdentifier> _parents = new SplayTreeMap();
+
+ /// Declarations requested via `smoke.getDeclaration or `smoke.query`.
+ final Map<TypeIdentifier, Map<String, _DeclarationCode>> _declarations =
+ new SplayTreeMap();
+
+ /// Static methods used on each type.
+ final Map<TypeIdentifier, Set<String>> _staticMethods = new SplayTreeMap();
+
+ /// Names that are used both as strings and symbols.
+ final Set<String> _names = new SplayTreeSet();
+
+ /// Prefixes associated with imported libraries.
+ final Map<String, String> _libraryPrefix = {};
+
+ /// Register that [name] is used as a getter in the code.
+ void addGetter(String name) {
+ _getters.add(name);
+ }
+
+ /// Register that [name] is used as a setter in the code.
+ void addSetter(String name) {
+ _setters.add(name);
+ }
+
+ /// Register that [name] might be needed as a symbol.
+ void addSymbol(String name) {
+ _names.add(name);
+ }
+
+ /// Register that `cls.name` is used as a static method in the code.
+ void addStaticMethod(TypeIdentifier cls, String name) {
+ var methods =
+ _staticMethods.putIfAbsent(cls, () => new SplayTreeSet<String>());
+ _addLibrary(cls.importUrl);
+ methods.add(name);
+ }
+
+ int _mixins = 0;
+
+ /// Creates a new type to represent a mixin. Use [comment] to help users
+ /// figure out what mixin is being represented.
+ TypeIdentifier createMixinType([String comment = '']) =>
+ new TypeIdentifier(null, '_M${_mixins++}', comment);
+
+ /// Register that we care to know that [child] extends [parent].
+ void addParent(TypeIdentifier child, TypeIdentifier parent) {
+ var existing = _parents[child];
+ if (existing != null) {
+ if (existing == parent) return;
+ throw new StateError('$child already has a different parent associated'
+ '($existing instead of $parent)');
+ }
+ _addLibrary(child.importUrl);
+ _addLibrary(parent.importUrl);
+ _parents[child] = parent;
+ }
+
+ /// Register a declaration of a field, property, or method. Note that one and
+ /// only one of [isField], [isMethod], or [isProperty] should be set at a
+ /// given time.
+ void addDeclaration(TypeIdentifier cls, String name, TypeIdentifier type,
+ {bool isField: false,
+ bool isProperty: false,
+ bool isMethod: false,
+ bool isFinal: false,
+ bool isStatic: false,
+ List<ConstExpression> annotations: const []}) {
+ final count = (isField ? 1 : 0) + (isProperty ? 1 : 0) + (isMethod ? 1 : 0);
+ if (count != 1) {
+ throw new ArgumentError('Declaration must be one (and only one) of the '
+ 'following: a field, a property, or a method.');
+ }
+ var kind = isField ? 'FIELD' : isProperty ? 'PROPERTY' : 'METHOD';
+ _declarations.putIfAbsent(
+ cls, () => new SplayTreeMap<String, _DeclarationCode>());
+ _addLibrary(cls.importUrl);
+ var map = _declarations[cls];
+
+ for (var exp in annotations) {
+ for (var lib in exp.librariesUsed) {
+ _addLibrary(lib);
+ }
+ }
+
+ _addLibrary(type.importUrl);
+ var decl =
+ new _DeclarationCode(name, type, kind, isFinal, isStatic, annotations);
+ if (map.containsKey(name) && map[name] != decl) {
+ throw new StateError('$type.$name already has a different declaration'
+ ' (${map[name]} instead of $decl).');
+ }
+ map[name] = decl;
+ }
+
+ /// Register that we might try to read declarations of [type], even if no
+ /// declaration exists. This informs the smoke system that querying for a
+ /// member in this class might be intentional and not an error.
+ void addEmptyDeclaration(TypeIdentifier type) {
+ _addLibrary(type.importUrl);
+ _declarations.putIfAbsent(
+ type, () => new SplayTreeMap<String, _DeclarationCode>());
+ }
+
+ /// Writes to [buffer] a line for each import that is needed by the generated
+ /// code. The code added by [writeStaticConfiguration] depends on these
+ /// imports.
+ void writeImports(StringBuffer buffer) {
+ DEFAULT_IMPORTS.forEach((i) => buffer.writeln(i));
+ _libraryPrefix.forEach((url, prefix) {
+ buffer.writeln("import '$url' as $prefix;");
+ });
+ }
+
+ /// Writes to [buffer] top-level declarations that are used by the code
+ /// generated in [writeStaticConfiguration]. These are typically declarations
+ /// of empty classes that are then used as placeholders for mixin
+ /// superclasses.
+ void writeTopLevelDeclarations(StringBuffer buffer) {
+ var types = new Set()
+ ..addAll(_parents.keys)
+ ..addAll(_parents.values)
+ ..addAll(_declarations.keys)
+ ..addAll(_declarations.values.expand((m) => m.values.map((d) => d.type)))
+ ..removeWhere((t) => t.importUrl != null);
+ for (var type in types) {
+ buffer.write('abstract class ${type.name} {}');
+ if (type.comment != null) buffer.write(' // ${type.comment}');
+ buffer.writeln();
+ }
+ }
+
+ /// Appends to [buffer] code that will create smoke's static configuration.
+ /// For example, the code might be of the form:
+ ///
+ /// new StaticConfiguration(
+ /// getters: {
+ /// #i: (o) => o.i,
+ /// ...
+ /// names: {
+ /// #i: "i",
+ /// })
+ ///
+ /// Callers of this code can assign this expression to a variable, and should
+ /// generate code that invokes `useGeneratedCode`.
+ ///
+ /// The optional [indent] argument is used for formatting purposes. All
+ /// entries in each map (getters, setters, names, declarations, parents) are
+ /// sorted alphabetically.
+ ///
+ /// **Note**: this code assumes that imports from [writeImports] and top-level
+ /// declarations from [writeTopLevelDeclarations] are included in the same
+ /// library where this code will live.
+ void writeStaticConfiguration(StringBuffer buffer, [int indent = 2]) {
+ final spaces = ' ' * (indent + 4);
+ var args = {};
+
+ if (_getters.isNotEmpty) {
+ args['getters'] = _getters.map((n) => '${_symbol(n)}: (o) => o.$n');
+ }
+ if (_setters.isNotEmpty) {
+ args['setters'] =
+ _setters.map((n) => '${_symbol(n)}: (o, v) { o.$n = v; }');
+ }
+
+ if (_parents.isNotEmpty) {
+ var parentsMap = [];
+ _parents.forEach((child, parent) {
+ var parent = _parents[child];
+ parentsMap.add('${child.asCode(_libraryPrefix)}: '
+ '${parent.asCode(_libraryPrefix)}');
+ });
+ args['parents'] = parentsMap;
+ }
+
+ if (_declarations.isNotEmpty) {
+ var declarations = [];
+ _declarations.forEach((type, members) {
+ final sb = new StringBuffer()
+ ..write(type.asCode(_libraryPrefix))
+ ..write(': ');
+ if (members.isEmpty) {
+ sb.write('{}');
+ } else {
+ sb.write('{\n');
+ members.forEach((name, decl) {
+ var decl = members[name].asCode(_libraryPrefix);
+ sb.write('${spaces} ${_symbol(name)}: $decl,\n');
+ });
+ sb.write('${spaces} }');
+ }
+ declarations.add(sb.toString());
+ });
+ args['declarations'] = declarations;
+ }
+
+ if (_staticMethods.isNotEmpty) {
+ var methods = [];
+ _staticMethods.forEach((type, members) {
+ var className = type.asCode(_libraryPrefix);
+ final sb = new StringBuffer()..write(className)..write(': ');
+ if (members.isEmpty) {
+ sb.write('{}');
+ } else {
+ sb.write('{\n');
+ for (var name in members) {
+ sb.write('${spaces} ${_symbol(name)}: $className.$name,\n');
+ }
+ sb.write('${spaces} }');
+ }
+ methods.add(sb.toString());
+ });
+ args['staticMethods'] = methods;
+ }
+
+ if (_names.isNotEmpty) {
+ args['names'] = _names.map((n) => "${_symbol(n)}: r'$n'");
+ }
+
+ buffer
+ ..writeln('new StaticConfiguration(')
+ ..write('${spaces}checkedMode: false');
+
+ args.forEach((name, mapContents) {
+ buffer.writeln(',');
+ // TODO(sigmund): use const map when Type can be keys (dartbug.com/17123)
+ buffer.writeln('${spaces}$name: {');
+ for (var entry in mapContents) {
+ buffer.writeln('${spaces} $entry,');
+ }
+ buffer.write('${spaces}}');
+ });
+ buffer.write(')');
+ }
+
+ /// Adds a library that needs to be imported.
+ void _addLibrary(String url) {
+ if (url == null || url == 'dart:core') return;
+ _libraryPrefix.putIfAbsent(url, () => 'smoke_${_libraryPrefix.length}');
+ }
+}
+
+/// Information used to generate code that allocates a `Declaration` object.
+class _DeclarationCode extends ConstExpression {
+ final String name;
+ final TypeIdentifier type;
+ final String kind;
+ final bool isFinal;
+ final bool isStatic;
+ final List<ConstExpression> annotations;
+
+ _DeclarationCode(this.name, this.type, this.kind, this.isFinal, this.isStatic,
+ this.annotations);
+
+ List<String> get librariesUsed => <String>[]
+ ..addAll(type.librariesUsed)
+ ..addAll(annotations.expand/*<String>*/((a) => a.librariesUsed));
+
+ String asCode(Map<String, String> libraryPrefixes) {
+ var sb = new StringBuffer();
+ sb.write('const Declaration(${_symbol(name)}, '
+ '${type.asCode(libraryPrefixes)}');
+ if (kind != 'FIELD') sb.write(', kind: $kind');
+ if (isFinal) sb.write(', isFinal: true');
+ if (isStatic) sb.write(', isStatic: true');
+ if (annotations != null && annotations.isNotEmpty) {
+ sb.write(', annotations: const [');
+ bool first = true;
+ for (var e in annotations) {
+ if (!first) sb.write(', ');
+ first = false;
+ sb.write(e.asCode(libraryPrefixes));
+ }
+ sb.write(']');
+ }
+ sb.write(')');
+ return sb.toString();
+ }
+
+ String toString() =>
+ '(decl: $type.$name - $kind, $isFinal, $isStatic, $annotations)';
+ operator ==(other) =>
+ other is _DeclarationCode &&
+ name == other.name &&
+ type == other.type &&
+ kind == other.kind &&
+ isFinal == other.isFinal &&
+ isStatic == other.isStatic &&
+ compareLists(annotations, other.annotations);
+ int get hashCode => name.hashCode + (31 * type.hashCode);
+}
+
+/// A constant expression that can be used as an annotation.
+abstract class ConstExpression {
+ /// Returns the library URLs that needs to be imported for this
+ /// [ConstExpression] to be a valid annotation.
+ List<String> get librariesUsed;
+
+ /// Return a string representation of the code in this expression.
+ /// [libraryPrefixes] describes what prefix has been associated with each
+ /// import url mentioned in [libraryUsed].
+ String asCode(Map<String, String> libraryPrefixes);
+
+ ConstExpression();
+
+ /// Create a string expression of the form `'string'`, where [string] is
+ /// normalized so we can correctly wrap it in single quotes.
+ factory ConstExpression.string(String string) {
+ var value = string.replaceAll(r'\', r'\\').replaceAll(r"'", r"\'");
+ return new CodeAsConstExpression("'$value'");
+ }
+
+ /// Create an expression of the form `prefix.variable_name`.
+ factory ConstExpression.identifier(String importUrl, String name) =>
+ new TopLevelIdentifier(importUrl, name);
+
+ /// Create an expression of the form `prefix.Constructor(v1, v2, p3: v3)`.
+ factory ConstExpression.constructor(
+ String importUrl,
+ String name,
+ List<ConstExpression> positionalArgs,
+ Map<String, ConstExpression> namedArgs) =>
+ new ConstructorExpression(importUrl, name, positionalArgs, namedArgs);
+}
+
+/// A constant expression written as a String. Used when the code is self
+/// contained and it doesn't depend on any imported libraries.
+class CodeAsConstExpression extends ConstExpression {
+ String code;
+ List<String> get librariesUsed => const [];
+
+ CodeAsConstExpression(this.code);
+
+ String asCode(Map<String, String> libraryPrefixes) => code;
+
+ String toString() => '(code: $code)';
+ operator ==(other) => other is CodeAsConstExpression && code == other.code;
+ int get hashCode => code.hashCode;
+}
+
+/// Describes a reference to some symbol that is exported from a library. This
+/// is typically used to refer to a type or a top-level variable from that
+/// library.
+class TopLevelIdentifier extends ConstExpression {
+ final String importUrl;
+ final String name;
+ TopLevelIdentifier(this.importUrl, this.name);
+
+ List<String> get librariesUsed => [importUrl];
+ String asCode(Map<String, String> libraryPrefixes) {
+ if (importUrl == 'dart:core' || importUrl == null) {
+ // TODO(jmesserly): analyzer doesn't consider `dynamic` to be a constant,
+ // so use Object as a workaround:
+ // https://code.google.com/p/dart/issues/detail?id=22989
+ return name == 'dynamic' ? 'Object' : name;
+ }
+ return '${libraryPrefixes[importUrl]}.$name';
+ }
+
+ String toString() => '(identifier: $importUrl, $name)';
+ operator ==(other) =>
+ other is TopLevelIdentifier &&
+ name == other.name &&
+ importUrl == other.importUrl;
+ int get hashCode => 31 * importUrl.hashCode + name.hashCode;
+}
+
+/// Represents an expression that invokes a const constructor.
+class ConstructorExpression extends ConstExpression {
+ final String importUrl;
+ final String name;
+ final List<ConstExpression> positionalArgs;
+ final Map<String, ConstExpression> namedArgs;
+ ConstructorExpression(
+ this.importUrl, this.name, this.positionalArgs, this.namedArgs);
+
+ List<String> get librariesUsed => <String>[importUrl]
+ ..addAll(positionalArgs.expand/*<String>*/((e) => e.librariesUsed))
+ ..addAll(namedArgs.values.expand/*<String>*/((e) => e.librariesUsed));
+
+ String asCode(Map<String, String> libraryPrefixes) {
+ var sb = new StringBuffer();
+ sb.write('const ');
+ if (importUrl != 'dart:core' && importUrl != null) {
+ sb.write('${libraryPrefixes[importUrl]}.');
+ }
+ sb.write('$name(');
+ bool first = true;
+ for (var e in positionalArgs) {
+ if (!first) sb.write(', ');
+ first = false;
+ sb.write(e.asCode(libraryPrefixes));
+ }
+ namedArgs.forEach((name, value) {
+ if (!first) sb.write(', ');
+ first = false;
+ sb.write('$name: ');
+ sb.write(value.asCode(libraryPrefixes));
+ });
+ sb.write(')');
+ return sb.toString();
+ }
+
+ String toString() => '(ctor: $importUrl, $name, $positionalArgs, $namedArgs)';
+ operator ==(other) =>
+ other is ConstructorExpression &&
+ name == other.name &&
+ importUrl == other.importUrl &&
+ compareLists(positionalArgs, other.positionalArgs) &&
+ compareMaps(namedArgs, other.namedArgs);
+ int get hashCode => 31 * importUrl.hashCode + name.hashCode;
+}
+
+/// Describes a type identifier, with the library URL where the type is defined.
+// TODO(sigmund): consider adding support for imprecise TypeIdentifiers, which
+// may be used by tools that want to generate code without using the analyzer
+// (they can syntactically tell the type comes from one of N imports).
+class TypeIdentifier extends TopLevelIdentifier
+ implements Comparable<TypeIdentifier> {
+ final String comment;
+ TypeIdentifier(importUrl, typeName, [this.comment])
+ : super(importUrl, typeName);
+
+ // We implement [Comparable] to sort out entries in the generated code.
+ int compareTo(TypeIdentifier other) {
+ if (importUrl == null && other.importUrl != null) return 1;
+ if (importUrl != null && other.importUrl == null) return -1;
+ var c1 = importUrl == null ? 0 : importUrl.compareTo(other.importUrl);
+ return c1 != 0 ? c1 : name.compareTo(other.name);
+ }
+
+ String toString() => '(type-identifier: $importUrl, $name, $comment)';
+ bool operator ==(other) =>
+ other is TypeIdentifier &&
+ importUrl == other.importUrl &&
+ name == other.name &&
+ comment == other.comment;
+ int get hashCode => super.hashCode;
+}
+
+/// Default set of imports added by [SmokeCodeGenerator].
+const DEFAULT_IMPORTS = const [
+ "import 'package:smoke/smoke.dart' show Declaration, PROPERTY, METHOD;",
+ "import 'package:smoke/static.dart' show "
+ "useGeneratedCode, StaticConfiguration;",
+];
+
+_symbol(String name) {
+ if (!_publicSymbolPattern.hasMatch(name)) {
+ throw new StateError('invalid symbol name: "$name"');
+ }
+ return _literalSymbolPattern.hasMatch(name)
+ ? '#$name'
+ : "const Symbol('$name')";
+}
+
+// TODO(sigmund): is this included in some library we can import? I derived the
+// definitions below from sdk/lib/internal/symbol.dart.
+
+/// Reserved words in Dart.
+const String _reservedWordRE =
+ r'(?:assert|break|c(?:a(?:se|tch)|lass|on(?:st|tinue))|d(?:efault|o)|'
+ r'e(?:lse|num|xtends)|f(?:alse|inal(?:ly)?|or)|i[fns]|n(?:ew|ull)|'
+ r'ret(?:hrow|urn)|s(?:uper|witch)|t(?:h(?:is|row)|r(?:ue|y))|'
+ r'v(?:ar|oid)|w(?:hile|ith))';
+
+/// Public identifier: a valid identifier (not a reserved word) that doesn't
+/// start with '_'.
+const String _publicIdentifierRE =
+ r'(?!' '$_reservedWordRE' r'\b(?!\$))[a-zA-Z$][\w$]*';
+
+/// Pattern that matches operators only.
+final RegExp _literalSymbolPattern =
+ new RegExp('^(?:$_publicIdentifierRE(?:\$|[.](?!\$)))+?\$');
+
+/// Operator names allowed as symbols. The name of the oeprators is the same as
+/// the operator itself except for unary minus, where the name is "unary-".
+const String _operatorRE =
+ r'(?:[\-+*/%&|^]|\[\]=?|==|~/?|<[<=]?|>[>=]?|unary-)';
+
+/// Pattern that matches public symbols.
+final RegExp _publicSymbolPattern = new RegExp(
+ '^(?:$_operatorRE\$|$_publicIdentifierRE(?:=?\$|[.](?!\$)))+?\$');
diff --git a/pub/smoke/lib/codegen/recorder.dart b/pub/smoke/lib/codegen/recorder.dart
new file mode 100644
index 0000000..7d41029
--- /dev/null
+++ b/pub/smoke/lib/codegen/recorder.dart
@@ -0,0 +1,415 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Records accesses to Dart program declarations and generates code that will
+/// allow to do the same accesses at runtime using `package:smoke/static.dart`.
+/// Internally, this library relies on the `analyzer` to extract data from the
+/// program, and then uses [SmokeCodeGenerator] to produce the code needed by
+/// the smoke system.
+///
+/// This library only uses the analyzer to consume data previously produced by
+/// running the resolver. This library does not provide any hooks to integrate
+/// running the analyzer itself. See `package:code_transformers` to integrate
+/// the analyzer into pub transformers.
+library smoke.codegen.recorder;
+
+import 'package:analyzer/src/generated/element.dart';
+import 'package:analyzer/src/generated/ast.dart';
+import 'generator.dart';
+
+typedef String ImportUrlResolver(LibraryElement lib);
+
+/// A recorder that tracks how elements are accessed in order to generate code
+/// that replicates those accesses with the smoke runtime.
+class Recorder {
+ /// Underlying code generator.
+ SmokeCodeGenerator generator;
+
+ /// Function that provides the import url for a library element. This may
+ /// internally use the resolver to resolve the import url.
+ ImportUrlResolver importUrlFor;
+
+ Recorder(this.generator, this.importUrlFor);
+
+ /// Stores mixins that have been recorded and associates a type identifier
+ /// with them. Mixins don't have an associated identifier in the code, so we
+ /// generate a unique identifier for them and use it throughout the code.
+ Map<TypeIdentifier, Map<ClassElement, TypeIdentifier>> _mixins = {};
+
+ /// Adds the superclass information of [type] (including intermediate mixins).
+ /// This will not generate data for direct subtypes of Object, as that is
+ /// considered redundant information.
+ void lookupParent(ClassElement type) {
+ var parent = type.supertype;
+ var mixins = type.mixins;
+ if (parent == null && mixins.isEmpty) return; // type is Object
+ var baseType = parent.element;
+ var baseId = _typeFor(baseType);
+ if (mixins.isNotEmpty) {
+ _mixins.putIfAbsent(baseId, () => {});
+ for (var m in mixins) {
+ var mixinType = m.element;
+ var mixinId = _mixins[baseId].putIfAbsent(mixinType, () {
+ var comment = '${baseId.name} & ${mixinType.name}';
+ return generator.createMixinType(comment);
+ });
+ if (!baseType.type.isObject) generator.addParent(mixinId, baseId);
+ baseType = mixinType;
+ baseId = mixinId;
+ _mixins.putIfAbsent(mixinId, () => {});
+ }
+ }
+ if (!baseType.type.isObject) generator.addParent(_typeFor(type), baseId);
+ }
+
+ TypeIdentifier _typeFor(Element type) => new TypeIdentifier(
+ type.library == null ? 'dart:core' : importUrlFor(type.library),
+ type.displayName);
+
+ /// Adds any declaration and superclass information that is needed to answer a
+ /// query on [type] that matches [options]. Also adds symbols, getters, and
+ /// setters if [includeAccessors] is true. If [results] is not null, it will
+ /// be filled up with the members that match the query.
+ void runQuery(ClassElement type, QueryOptions options,
+ {bool includeAccessors: true, List results}) {
+ if (type.type.isObject) return; // We don't include Object in query results.
+ var id = _typeFor(type);
+ var parent = type.supertype != null ? type.supertype.element : null;
+ if (options.includeInherited &&
+ parent != null &&
+ parent != options.includeUpTo) {
+ lookupParent(type);
+ runQuery(parent, options, includeAccessors: includeAccessors);
+ var parentId = _typeFor(parent);
+ for (var m in type.mixins) {
+ var mixinClass = m.element;
+ var mixinId = _mixins[parentId][mixinClass];
+ _runQueryInternal(
+ mixinClass, mixinId, options, includeAccessors, results);
+ parentId = mixinId;
+ }
+ }
+ _runQueryInternal(type, id, options, includeAccessors, results);
+ }
+
+ /// Helper for [runQuery]. This runs the query only on a specific [type],
+ /// which could be a class or a mixin labeled by [id].
+ // TODO(sigmund): currently we materialize mixins in smoke/static.dart,
+ // we should consider to include the mixin declaration information directly,
+ // and remove the duplication we have for mixins today.
+ void _runQueryInternal(ClassElement type, TypeIdentifier id,
+ QueryOptions options, bool includeAccessors, List results) {
+ skipBecauseOfAnnotations(Element e) {
+ if (options.withAnnotations == null) return false;
+ return !_matchesAnnotation(e.metadata, options.withAnnotations);
+ }
+
+ if (options.includeFields) {
+ for (var f in type.fields) {
+ if (f.isStatic) continue;
+ if (f.isSynthetic) continue; // exclude getters
+ if (options.excludeFinal && f.isFinal) continue;
+ var name = f.displayName;
+ if (options.matches != null && !options.matches(name)) continue;
+ if (skipBecauseOfAnnotations(f)) continue;
+ if (results != null) results.add(f);
+ generator.addDeclaration(id, name, _typeFor(f.type.element),
+ isField: true,
+ isFinal: f.isFinal,
+ annotations: _copyAnnotations(f));
+ if (includeAccessors) _addAccessors(name, !f.isFinal);
+ }
+ }
+
+ if (options.includeProperties) {
+ for (var a in type.accessors) {
+ if (a is! PropertyAccessorElement) continue;
+ if (a.isStatic || !a.isGetter) continue;
+ var v = a.variable;
+ if (v is FieldElement && !v.isSynthetic) continue; // exclude fields
+ if (options.excludeFinal && v.isFinal) continue;
+ var name = v.displayName;
+ if (options.matches != null && !options.matches(name)) continue;
+ if (skipBecauseOfAnnotations(a)) continue;
+ if (results != null) results.add(a);
+ generator.addDeclaration(id, name, _typeFor(a.type.returnType.element),
+ isProperty: true,
+ isFinal: v.isFinal,
+ annotations: _copyAnnotations(a));
+ if (includeAccessors) _addAccessors(name, !v.isFinal);
+ }
+ }
+
+ if (options.includeMethods) {
+ for (var m in type.methods) {
+ if (m.isStatic) continue;
+ var name = m.displayName;
+ if (options.matches != null && !options.matches(name)) continue;
+ if (skipBecauseOfAnnotations(m)) continue;
+ if (results != null) results.add(m);
+ generator.addDeclaration(
+ id, name, new TypeIdentifier('dart:core', 'Function'),
+ isMethod: true, annotations: _copyAnnotations(m));
+ if (includeAccessors) _addAccessors(name, false);
+ }
+ }
+ }
+
+ /// Adds the declaration of [name] if it was found in [type]. If [recursive]
+ /// is true, then we continue looking up [name] in the parent classes until we
+ /// find it or we reach [includeUpTo] or Object. Returns whether the
+ /// declaration was found. When a declaration is found, add also a symbol,
+ /// getter, and setter if [includeAccessors] is true.
+ bool lookupMember(ClassElement type, String name,
+ {bool recursive: false,
+ bool includeAccessors: true,
+ ClassElement includeUpTo}) =>
+ _lookupMemberInternal(
+ type, _typeFor(type), name, recursive, includeAccessors, includeUpTo);
+
+ /// Helper for [lookupMember] that walks up the type hierarchy including mixin
+ /// classes.
+ bool _lookupMemberInternal(ClassElement type, TypeIdentifier id, String name,
+ bool recursive, bool includeAccessors, ClassElement includeUpTo) {
+ // Exclude members from [Object].
+ if (type.type.isObject) return false;
+ generator.addEmptyDeclaration(id);
+ for (var f in type.fields) {
+ if (f.displayName != name) continue;
+ if (f.isSynthetic) continue; // exclude getters
+ generator.addDeclaration(id, name, _typeFor(f.type.element),
+ isField: true,
+ isFinal: f.isFinal,
+ isStatic: f.isStatic,
+ annotations: _copyAnnotations(f));
+ if (includeAccessors && !f.isStatic) _addAccessors(name, !f.isFinal);
+ return true;
+ }
+
+ for (var a in type.accessors) {
+ if (a is! PropertyAccessorElement) continue;
+ // TODO(sigmund): support setters without getters.
+ if (!a.isGetter) continue;
+ if (a.displayName != name) continue;
+ var v = a.variable;
+ if (v is FieldElement && !v.isSynthetic) continue; // exclude fields
+ generator.addDeclaration(id, name, _typeFor(a.type.returnType.element),
+ isProperty: true,
+ isFinal: v.isFinal,
+ isStatic: a.isStatic,
+ annotations: _copyAnnotations(a));
+ if (includeAccessors && !v.isStatic) _addAccessors(name, !v.isFinal);
+ return true;
+ }
+
+ for (var m in type.methods) {
+ if (m.displayName != name) continue;
+ generator.addDeclaration(
+ id, name, new TypeIdentifier('dart:core', 'Function'),
+ isMethod: true,
+ isStatic: m.isStatic,
+ annotations: _copyAnnotations(m));
+ if (includeAccessors) {
+ if (m.isStatic) {
+ generator.addStaticMethod(id, name);
+ generator.addSymbol(name);
+ } else {
+ _addAccessors(name, false);
+ }
+ }
+ return true;
+ }
+
+ if (recursive) {
+ lookupParent(type);
+ var parent = type.supertype != null ? type.supertype.element : null;
+ if (parent == null || parent == includeUpTo) return false;
+ var parentId = _typeFor(parent);
+ for (var m in type.mixins) {
+ var mixinClass = m.element;
+ var mixinId = _mixins[parentId][mixinClass];
+ if (_lookupMemberInternal(
+ mixinClass, mixinId, name, false, includeAccessors, includeUpTo)) {
+ return true;
+ }
+ parentId = mixinId;
+ }
+ return _lookupMemberInternal(
+ parent, parentId, name, true, includeAccessors, includeUpTo);
+ }
+ return false;
+ }
+
+ /// Add information so smoke can invoke the static method [type].[name].
+ void addStaticMethod(ClassElement type, String name) {
+ generator.addStaticMethod(_typeFor(type), name);
+ }
+
+ /// Adds [name] as a symbol, a getter, and optionally a setter in [generator].
+ _addAccessors(String name, bool includeSetter) {
+ generator.addSymbol(name);
+ generator.addGetter(name);
+ if (includeSetter) generator.addSetter(name);
+ }
+
+ /// Copy metadata associated with the declaration of [target].
+ List<ConstExpression> _copyAnnotations(Element target) {
+ var node = target.computeNode();
+ // [node] is the initialization expression, we walk up to get to the actual
+ // member declaration where the metadata is attached to.
+ while (node is! ClassMember) node = node.parent;
+ return (node as ClassMember)
+ .metadata
+ .map/*<ConstExpression>*/(_convertAnnotation)
+ .toList();
+ }
+
+ /// Converts annotations into [ConstExpression]s supported by the codegen
+ /// library.
+ ConstExpression _convertAnnotation(Annotation annotation) {
+ var element = annotation.element;
+ if (element is ConstructorElement) {
+ if (!element.name.isEmpty) {
+ throw new UnimplementedError(
+ 'named constructors are not implemented in smoke.codegen.recorder');
+ }
+
+ var positionalArgs = <ConstExpression>[];
+ var namedArgs = <String, ConstExpression>{};
+ for (var arg in annotation.arguments.arguments) {
+ if (arg is NamedExpression) {
+ namedArgs[arg.name.label.name] = _convertExpression(arg.expression);
+ } else {
+ positionalArgs.add(_convertExpression(arg));
+ }
+ }
+
+ return new ConstructorExpression(importUrlFor(element.library),
+ element.enclosingElement.name, positionalArgs, namedArgs);
+ }
+
+ if (element is PropertyAccessorElement) {
+ return new TopLevelIdentifier(
+ importUrlFor(element.library), element.name);
+ }
+
+ throw new UnsupportedError('unsupported annotation $annotation');
+ }
+
+ /// Converts [expression] into a [ConstExpression].
+ ConstExpression _convertExpression(Expression expression) {
+ if (expression is StringLiteral) {
+ return new ConstExpression.string(expression.stringValue);
+ }
+
+ if (expression is BooleanLiteral ||
+ expression is DoubleLiteral ||
+ expression is IntegerLiteral ||
+ expression is NullLiteral) {
+ return new CodeAsConstExpression("${(expression as dynamic).value}");
+ }
+
+ if (expression is Identifier) {
+ var element = expression.bestElement;
+ if (element == null || !element.isPublic) {
+ throw new UnsupportedError('private constants are not supported');
+ }
+
+ var url = importUrlFor(element.library);
+ if (element is ClassElement) {
+ return new TopLevelIdentifier(url, element.name);
+ }
+
+ if (element is PropertyAccessorElement) {
+ var variable = element.variable;
+ if (variable is FieldElement) {
+ var cls = variable.enclosingElement;
+ return new TopLevelIdentifier(url, '${cls.name}.${variable.name}');
+ } else if (variable is TopLevelVariableElement) {
+ return new TopLevelIdentifier(url, variable.name);
+ }
+ }
+ }
+
+ throw new UnimplementedError('expression convertion not implemented in '
+ 'smoke.codegen.recorder (${expression.runtimeType} $expression)');
+ }
+}
+
+/// Returns whether [metadata] contains any annotation that is either equal to
+/// an annotation in [queryAnnotations] or whose type is a subclass of a type
+/// listed in [queryAnnotations]. This is equivalent to the check done in
+/// `src/common.dart#matchesAnnotation`, except that this is applied to
+/// static metadata as it was provided by the analyzer.
+bool _matchesAnnotation(
+ Iterable<ElementAnnotation> metadata, Iterable<Element> queryAnnotations) {
+ for (var meta in metadata) {
+ var element = meta.element;
+ var exp;
+ var type;
+ if (element is PropertyAccessorElement) {
+ exp = element.variable;
+ type = exp.evaluationResult.value.type;
+ } else if (element is ConstructorElement) {
+ exp = element;
+ type = element.enclosingElement.type;
+ } else {
+ throw new UnimplementedError('Unsupported annotation: ${meta}');
+ }
+ for (var queryMeta in queryAnnotations) {
+ if (exp == queryMeta) return true;
+ if (queryMeta is ClassElement && type.isSubtypeOf(queryMeta.type)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/// Options equivalent to `smoke.dart#QueryOptions`, except that type
+/// information and annotations are denoted by resolver's elements.
+class QueryOptions {
+ /// Whether to include fields (default is true).
+ final bool includeFields;
+
+ /// Whether to include getters and setters (default is true). Note that to
+ /// include fields you also need to enable [includeFields].
+ final bool includeProperties;
+
+ /// Whether to include symbols from the given type and its superclasses
+ /// (except [Object]).
+ final bool includeInherited;
+
+ /// If [includeInherited], walk up the type hierarchy up to this type
+ /// (defaults to [Object]).
+ final ClassElement includeUpTo;
+
+ /// Whether to include final fields and getter-only properties.
+ final bool excludeFinal;
+
+ /// Whether to include methods (default is false).
+ final bool includeMethods;
+
+ /// If [withAnnotation] is not null, then it should be a list of types, so
+ /// only symbols that are annotated with instances of those types are
+ /// included.
+ final List<Element> withAnnotations;
+
+ /// If [matches] is not null, then only those fields, properties, or methods
+ /// that match will be included.
+ final NameMatcher matches;
+
+ const QueryOptions(
+ {this.includeFields: true,
+ this.includeProperties: true,
+ this.includeInherited: true,
+ this.includeUpTo: null,
+ this.excludeFinal: false,
+ this.includeMethods: false,
+ this.withAnnotations: null,
+ this.matches: null});
+}
+
+/// Predicate that tells whether [name] should be included in query results.
+typedef bool NameMatcher(String name);
diff --git a/pub/smoke/lib/mirrors.dart b/pub/smoke/lib/mirrors.dart
new file mode 100644
index 0000000..a4ccf86
--- /dev/null
+++ b/pub/smoke/lib/mirrors.dart
@@ -0,0 +1,336 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Implementation of the smoke services using mirrors.
+library smoke.mirrors;
+
+import 'dart:mirrors';
+import 'package:smoke/smoke.dart';
+import 'package:logging/logging.dart';
+import 'src/common.dart';
+
+/// Set up the smoke package to use a mirror-based implementation. To tune what
+/// is preserved by `dart:mirrors`, use a @MirrorsUsed annotation and include
+/// 'smoke.mirrors' in your override arguments.
+useMirrors() {
+ configure(
+ new ReflectiveObjectAccessorService(),
+ new ReflectiveTypeInspectorService(),
+ new ReflectiveSymbolConverterService());
+}
+
+var _logger = new Logger('smoke.mirrors');
+
+/// Implements [ObjectAccessorService] using mirrors.
+class ReflectiveObjectAccessorService implements ObjectAccessorService {
+ read(Object object, Symbol name) => reflect(object).getField(name).reflectee;
+
+ void write(Object object, Symbol name, value) {
+ reflect(object).setField(name, value);
+ }
+
+ invoke(receiver, Symbol methodName, List args,
+ {Map namedArgs, bool adjust: false}) {
+ var receiverMirror;
+ var method;
+ if (receiver is Type && methodName != #toString) {
+ receiverMirror = reflectType(receiver);
+ method = receiverMirror.declarations[methodName];
+ } else {
+ receiverMirror = reflect(receiver);
+ method = _findMethod(receiverMirror.type, methodName);
+ }
+ if (method != null && adjust) {
+ var required = 0;
+ var optional = 0;
+ for (var p in method.parameters) {
+ if (p.isOptional) {
+ if (!p.isNamed) optional++;
+ } else {
+ required++;
+ }
+ }
+ args = adjustList(args, required, required + optional);
+ }
+ return receiverMirror.invoke(methodName, args, namedArgs).reflectee;
+ }
+}
+
+/// Implements [TypeInspectorService] using mirrors.
+class ReflectiveTypeInspectorService implements TypeInspectorService {
+ bool isSubclassOf(Type type, Type supertype) {
+ if (type == supertype || supertype == Object) return true;
+ // TODO(sigmund): change to mirror.isSubclassOf when it gets implemented in
+ // dart2js. (dartbug.com/12439)
+ var mirror = reflectClass(type);
+ var top = reflectClass(supertype);
+ while (mirror != _objectType) {
+ mirror = _safeSuperclass(mirror);
+ if (mirror == top) return true;
+ }
+ return false;
+ }
+
+ bool hasGetter(Type type, Symbol name) {
+ var reflectiveType = reflectType(type);
+ if (reflectiveType is! ClassMirror) return false;
+ var mirror = reflectiveType as ClassMirror;
+ while (mirror != _objectType) {
+ final members = mirror.declarations;
+ if (members.containsKey(name)) return true;
+ mirror = _safeSuperclass(mirror);
+ }
+ return false;
+ }
+
+ bool hasSetter(Type type, Symbol name) {
+ var reflectiveType = reflectType(type);
+ if (reflectiveType is! ClassMirror) return false;
+ var mirror = reflectiveType as ClassMirror;
+ var setterName = _setterName(name);
+ while (mirror != _objectType) {
+ final members = mirror.declarations;
+ var declaration = members[name];
+ if (declaration is VariableMirror && !declaration.isFinal) return true;
+ if (members.containsKey(setterName)) return true;
+ mirror = _safeSuperclass(mirror);
+ }
+ return false;
+ }
+
+ bool hasInstanceMethod(Type type, Symbol name) {
+ var reflectiveType = reflectType(type);
+ if (reflectiveType is! ClassMirror) return false;
+ var mirror = reflectiveType as ClassMirror;
+ while (mirror != _objectType) {
+ final m = mirror.declarations[name];
+ if (m is MethodMirror && m.isRegularMethod && !m.isStatic) return true;
+ mirror = _safeSuperclass(mirror);
+ }
+ return false;
+ }
+
+ bool hasStaticMethod(Type type, Symbol name) {
+ var reflectiveType = reflectType(type);
+ if (reflectiveType is! ClassMirror) return false;
+ var mirror = reflectiveType as ClassMirror;
+ final m = mirror.declarations[name];
+ return m is MethodMirror && m.isRegularMethod && m.isStatic;
+ }
+
+ Declaration getDeclaration(Type type, Symbol name) {
+ var reflectiveType = reflectType(type);
+ if (reflectiveType is! ClassMirror) return null;
+ var mirror = reflectiveType as ClassMirror;
+
+ var declaration;
+ while (mirror != _objectType) {
+ final members = mirror.declarations;
+ if (members.containsKey(name)) {
+ declaration = members[name];
+ break;
+ }
+ mirror = _safeSuperclass(mirror);
+ }
+ if (declaration == null) {
+ _logger.severe("declaration doesn't exists ($type.$name).");
+ return null;
+ }
+ return new _MirrorDeclaration(mirror, declaration);
+ }
+
+ List<Declaration> query(Type type, QueryOptions options) {
+ var mirror = reflectType(type);
+ if (mirror is! ClassMirror) return null;
+ return _query(mirror, options);
+ }
+
+ List<Declaration> _query(ClassMirror cls, QueryOptions options) {
+ final visitParent = options.includeInherited &&
+ cls.superclass != null &&
+ // TODO(sigmund): use _toType(cls.superclass) != options.includeUpTo
+ // when dartbug.com/16925 gets fixed (_toType fails in dart2js if
+ // applied to classes with type-arguments).
+ cls.superclass != reflectClass(options.includeUpTo);
+ var result =
+ visitParent ? _query(cls.superclass, options) : <Declaration>[];
+ for (var member in cls.declarations.values) {
+ if (member is! VariableMirror && member is! MethodMirror) continue;
+ if (member.isPrivate || (member as dynamic).isStatic) continue;
+ var name = member.simpleName;
+ if (member is VariableMirror) {
+ if (!options.includeFields) continue;
+ if (options.excludeFinal && member.isFinal) continue;
+ }
+
+ // TODO(sigmund): what if we have a setter but no getter?
+ if (member is MethodMirror && member.isSetter) continue;
+ if (member is MethodMirror && member.isConstructor) continue;
+
+ if (member is MethodMirror && member.isGetter) {
+ if (!options.includeProperties) continue;
+ if (options.excludeFinal && !_hasSetter(cls, member)) continue;
+ }
+
+ if (member is MethodMirror && member.isRegularMethod) {
+ if (!options.includeMethods) continue;
+ }
+
+ if (options.matches != null && !options.matches(name)) continue;
+
+ var annotations = member.metadata.map((m) => m.reflectee).toList();
+ if (options.withAnnotations != null &&
+ !matchesAnnotation(annotations, options.withAnnotations)) {
+ continue;
+ }
+
+ var declaration = new _MirrorDeclaration(cls, member);
+
+ if (options.excludeOverriden) {
+ result.retainWhere((value) => declaration.name != value.name);
+ }
+
+ // TODO(sigmund): should we cache parts of this declaration so we don't
+ // compute them twice? For example, this chould be `new Declaration(name,
+ // type, ...)` and we could reuse what we computed above to implement the
+ // query filtering. Note, when I tried to eagerly compute everything, I
+ // run into trouble with type (`type = _toType(member.type)`), dart2js
+ // failed when the underlying types had type-arguments (see
+ // dartbug.com/16925).
+ result.add(declaration);
+ }
+
+ return result;
+ }
+}
+
+/// Implements [SymbolConverterService] using mirrors.
+class ReflectiveSymbolConverterService implements SymbolConverterService {
+ String symbolToName(Symbol symbol) => MirrorSystem.getName(symbol);
+ Symbol nameToSymbol(String name) => new Symbol(name);
+}
+
+// TODO(jmesserly): workaround for:
+// https://code.google.com/p/dart/issues/detail?id=10029
+Symbol _setterName(Symbol getter) =>
+ new Symbol('${MirrorSystem.getName(getter)}=');
+
+ClassMirror _safeSuperclass(ClassMirror type) {
+ try {
+ var t = type.superclass;
+ // TODO(sigmund): workaround for darbug.com/17779.
+ // Interceptor is leaked by dart2js. It has the same methods as Object
+ // (including noSuchMethod), and our code above assumes that it doesn't
+ // exist. Most queries exclude Object, so they should exclude Interceptor
+ // too. We don't check for t.simpleName == #Interceptor because depending on
+ // dart2js optimizations it may be #Interceptor or #num/Interceptor.
+ // Checking for a private library seems to reliably filter this out.
+ if (t != null && t.owner != null && t.owner.isPrivate) {
+ t = _objectType;
+ }
+ return t;
+ } on UnsupportedError catch (_) {
+ // Note: dart2js throws UnsupportedError when the type is not reflectable.
+ return _objectType;
+ }
+}
+
+MethodMirror _findMethod(ClassMirror type, Symbol name) {
+ do {
+ var member = type.declarations[name];
+ if (member is MethodMirror) return member;
+ type = type.superclass;
+ } while (type != null);
+ return null;
+}
+
+// When recursively looking for symbols up the type-hierarchy it's generally a
+// good idea to stop at Object, since we know it doesn't have what we want.
+// TODO(jmesserly): This is also a workaround for what appears to be a V8
+// bug introduced between Chrome 31 and 32. After 32
+// JsClassMirror.declarations on Object calls
+// JsClassMirror.typeVariables, which tries to get the _jsConstructor's
+// .prototype["<>"]. This ends up getting the "" property instead, maybe
+// because "<>" doesn't exist, and gets ";" which then blows up because
+// the code later on expects a List of ints.
+final _objectType = reflectClass(Object);
+
+bool _hasSetter(ClassMirror cls, MethodMirror getter) {
+ var mirror = cls.declarations[_setterName(getter.simpleName)];
+ return mirror is MethodMirror && mirror.isSetter;
+}
+
+Type _toType(TypeMirror t) {
+ // TODO(sigmund): this line can go away after dartbug.com/16962
+ if (t == _objectType) return Object;
+ if (t is ClassMirror) return t.reflectedType;
+ if (t == null || t.qualifiedName != #dynamic) {
+ _logger.warning('unknown type ($t).');
+ }
+ return dynamic;
+}
+
+class _MirrorDeclaration implements Declaration {
+ final ClassMirror _cls;
+ final _original;
+
+ _MirrorDeclaration(this._cls, DeclarationMirror this._original);
+
+ Symbol get name => _original.simpleName;
+
+ DeclarationKind get kind => isField ? FIELD : isProperty ? PROPERTY : METHOD;
+
+ bool get isField => _original is VariableMirror;
+
+ bool get isProperty =>
+ _original is MethodMirror && !_original.isRegularMethod;
+
+ bool get isMethod => !isField && !isProperty;
+
+ /// If this is a property, whether it's read only (final fields or properties
+ /// with no setter).
+ bool get isFinal =>
+ (_original is VariableMirror && _original.isFinal) ||
+ (_original is MethodMirror &&
+ _original.isGetter &&
+ !_hasSetter(_cls, _original));
+
+ /// If this is a property, it's declared type (including dynamic if it's not
+ /// declared). For methods, the returned type.
+ Type get type {
+ if (_original is MethodMirror && _original.isRegularMethod) {
+ return Function;
+ }
+ var typeMirror =
+ _original is VariableMirror ? _original.type : _original.returnType;
+ return _toType(typeMirror);
+ }
+
+ /// Whether this symbol is static.
+ bool get isStatic => _original.isStatic;
+
+ /// List of annotations in this declaration.
+ List get annotations => _original.metadata.map((a) => a.reflectee).toList();
+
+ int get hashCode => name.hashCode;
+ operator ==(other) =>
+ other is Declaration &&
+ name == other.name &&
+ kind == other.kind &&
+ isFinal == other.isFinal &&
+ type == other.type &&
+ isStatic == other.isStatic &&
+ compareLists(annotations, other.annotations);
+ String toString() => (new StringBuffer()
+ ..write('(mirror-based-declaration ')
+ ..write(name)
+ ..write(isField
+ ? ' (field) '
+ : (isProperty ? ' (property) ' : ' (method) '))
+ ..write(isFinal ? 'final ' : '')
+ ..write(isStatic ? 'static ' : '')
+ ..write(annotations)
+ ..write(')'))
+ .toString();
+}
diff --git a/pub/smoke/lib/smoke.dart b/pub/smoke/lib/smoke.dart
new file mode 100644
index 0000000..afcc006
--- /dev/null
+++ b/pub/smoke/lib/smoke.dart
@@ -0,0 +1,286 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Collects services that can be used to access objects dynamically, inspect
+/// type information, and convert between symbols and strings.
+library smoke;
+
+import 'src/implementation.dart' as implementation;
+
+export 'src/common.dart' show minArgs, maxArgs, canAcceptNArgs, SUPPORTED_ARGS;
+import 'src/common.dart' show compareLists;
+
+/// Configures this library to use [objectAccessor] for all read/write/invoke
+/// APIs, [typeInspector] for all type query APIs, and [symbolConverter] for all
+/// symbol convertion operations.
+///
+/// This function doesn't need to be called during development, but frameworks
+/// should autogenerate a call to this function when running in deployment.
+void configure(
+ ObjectAccessorService objectAccessor,
+ TypeInspectorService typeInspector,
+ SymbolConverterService symbolConverter) {
+ implementation.objectAccessor = objectAccessor;
+ implementation.typeInspector = typeInspector;
+ implementation.symbolConverter = symbolConverter;
+}
+
+/// Return the value of [field] in [object].
+read(Object object, Symbol field) =>
+ implementation.objectAccessor.read(object, field);
+
+/// Update the [value] of [field] in [object].
+void write(Object object, Symbol field, value) =>
+ implementation.objectAccessor.write(object, field, value);
+
+/// Invoke [method] in [receiver] with [args]. The [receiver] can be either an
+/// object (to invoke instance methods) or a type (to invoke static methods).
+/// This function optionally [adjust]s the list of arguments to match the number
+/// of formal parameters by either adding nulls for missing arguments, or by
+/// truncating the list.
+invoke(receiver, Symbol method, List args,
+ {Map namedArgs, bool adjust: false}) =>
+ implementation.objectAccessor
+ .invoke(receiver, method, args, namedArgs: namedArgs, adjust: adjust);
+
+/// Tells whether [type] is transitively a subclass of [supertype].
+bool isSubclassOf(Type type, Type supertype) =>
+ implementation.typeInspector.isSubclassOf(type, supertype);
+
+// TODO(sigmund): consider adding also:
+// * isImplementationOf(type, subtype) to tells whether [type] declares that it
+// implements the [supertype] interface.
+// * isSubtypeOf(type, subtype): Tells whether [type]'s interface is a sybtype
+// of [supertype]. That is, whether it is a subclass or if [type] implements
+// [supertype].
+
+/// Tells whether [type] has a field or getter for [field].
+bool hasGetter(Type type, Symbol field) =>
+ implementation.typeInspector.hasGetter(type, field);
+
+/// Tells whether [type] has a field or setter for [field].
+bool hasSetter(Type type, Symbol field) =>
+ implementation.typeInspector.hasSetter(type, field);
+
+/// Tells whether [type] or a superclass (other than [Object]) defines
+/// `noSuchMethod`.
+bool hasNoSuchMethod(Type type) => hasInstanceMethod(type, #noSuchMethod);
+
+/// Tells whether [type] has or a superclass contains a specific instance
+/// [method] (excluding methods in [Object]).
+bool hasInstanceMethod(Type type, Symbol method) =>
+ implementation.typeInspector.hasInstanceMethod(type, method);
+
+/// Tells whether [type] has a specific static [method].
+bool hasStaticMethod(Type type, Symbol method) =>
+ implementation.typeInspector.hasStaticMethod(type, method);
+
+/// Get the declaration associated with field [name] found in [type] or a
+/// superclass of [type].
+Declaration getDeclaration(Type type, Symbol name) =>
+ implementation.typeInspector.getDeclaration(type, name);
+
+/// Retrieve all symbols of [type] that match [options].
+List<Declaration> query(Type type, QueryOptions options) =>
+ implementation.typeInspector.query(type, options);
+
+/// Returns the name associated with a [symbol].
+String symbolToName(Symbol symbol) =>
+ implementation.symbolConverter.symbolToName(symbol);
+
+/// Returns the symbol associated with a [name].
+Symbol nameToSymbol(String name) =>
+ implementation.symbolConverter.nameToSymbol(name);
+
+/// Establishes the parameters for [query] to search for symbols in a type
+/// hierarchy. For now only public instance symbols can be queried (no private,
+/// no static).
+class QueryOptions {
+ /// Whether to include fields (default is true).
+ final bool includeFields;
+
+ /// Whether to include getters and setters (default is true). Note that to
+ /// include fields you also need to enable [includeFields].
+ final bool includeProperties;
+
+ /// Whether to include symbols from the given type and its superclasses
+ /// (except [Object]).
+ final bool includeInherited;
+
+ /// If [includeInherited], walk up the type hierarchy up to this type
+ /// (defaults to [Object]).
+ final Type includeUpTo;
+
+ /// Whether to include final fields and getter-only properties.
+ final bool excludeFinal;
+
+ /// Whether to include symbols that are overriden within the subclass
+ /// (default is false).
+ final bool excludeOverriden;
+
+ /// Whether to include methods (default is false).
+ final bool includeMethods;
+
+ /// If [withAnnotation] is not null, then it should be a list of types, so
+ /// only symbols that are annotated with instances of those types are
+ /// included.
+ final List withAnnotations;
+
+ /// If [matches] is not null, then include only those fields, properties, or
+ /// methods that match the predicate.
+ final NameMatcher matches;
+
+ const QueryOptions(
+ {this.includeFields: true,
+ this.includeProperties: true,
+ this.includeInherited: true,
+ this.includeUpTo: Object,
+ this.excludeFinal: false,
+ this.excludeOverriden: false,
+ this.includeMethods: false,
+ this.withAnnotations: null,
+ this.matches: null});
+
+ String toString() => (new StringBuffer()
+ ..write('(options:')
+ ..write(includeFields ? 'fields ' : '')
+ ..write(includeProperties ? 'properties ' : '')
+ ..write(includeMethods ? 'methods ' : '')
+ ..write(includeInherited ? 'inherited ' : '_')
+ ..write(excludeFinal ? 'no finals ' : '')
+ ..write(excludeOverriden ? 'no overriden ' : '')
+ ..write('annotations: $withAnnotations')
+ ..write(matches != null ? 'with matcher' : '')
+ ..write(')'))
+ .toString();
+}
+
+/// Used to filter query results based on a predicate on [name]. Returns true if
+/// [name] should be included in the query results.
+typedef bool NameMatcher(Symbol name);
+
+/// Information associated with a symbol declaration (like a property or
+/// method).
+class Declaration {
+ /// Name of the property or method
+ final Symbol name;
+
+ /// Kind of declaration (field, property, or method).
+ final DeclarationKind kind;
+
+ /// Whether the symbol is a field (and not a getter/setter property).
+ bool get isField => kind == FIELD;
+
+ /// Whether the symbol is a getter/setter and not a field.
+ bool get isProperty => kind == PROPERTY;
+
+ /// Whether the symbol is a method.
+ bool get isMethod => kind == METHOD;
+
+ /// For fields, whether they are final, for properties, whether they are
+ /// read-only (they have no setter).
+ final bool isFinal;
+
+ /// If this is a field or property, it's declared type (including dynamic if
+ /// it's not declared). For methods, the returned type.
+ final Type type;
+
+ /// Whether this symbol is static.
+ final bool isStatic;
+
+ /// List of annotations in this declaration.
+ final List annotations;
+
+ const Declaration(this.name, this.type,
+ {this.kind: FIELD,
+ this.isFinal: false,
+ this.isStatic: false,
+ this.annotations: const []});
+
+ int get hashCode => name.hashCode;
+ operator ==(other) =>
+ other is Declaration &&
+ name == other.name &&
+ kind == other.kind &&
+ isFinal == other.isFinal &&
+ type == other.type &&
+ isStatic == other.isStatic &&
+ compareLists(annotations, other.annotations);
+
+ String toString() {
+ return (new StringBuffer()
+ ..write('(declaration ')
+ ..write(name)
+ ..write(isProperty ? ' (property) ' : ' (method) ')
+ ..write(isFinal ? 'final ' : '')
+ ..write(isStatic ? 'static ' : '')
+ ..write(annotations)
+ ..write(')'))
+ .toString();
+ }
+}
+
+/// Enumeration for declaration kinds (field, property, or method)
+class DeclarationKind {
+ final int kind;
+ const DeclarationKind(this.kind);
+}
+
+/// Declaration kind used to denote a raw field.
+const FIELD = const DeclarationKind(0);
+
+/// Declaration kind used to denote a getter/setter.
+const PROPERTY = const DeclarationKind(1);
+
+/// Declaration kind used to denote a method.
+const METHOD = const DeclarationKind(2);
+
+/// A service that provides a way to implement simple operations on objects like
+/// read, write, and invoke.
+abstract class ObjectAccessorService {
+ /// Return the value of [field] in [object].
+ read(Object object, Symbol field);
+
+ /// Update the [value] of [field] in [object].
+ void write(Object object, Symbol field, value);
+
+ /// Invoke [method] in [object] with [args]. It optionally [adjust]s the list
+ /// of arguments to match the number of formal parameters by either adding
+ /// nulls for missing arguments, or by truncating the list.
+ invoke(Object object, Symbol method, List args,
+ {Map namedArgs, bool adjust: false});
+}
+
+/// A service that provides partial inspection into Dart types.
+abstract class TypeInspectorService {
+ /// Tells whether [type] is transitively a subclass of [supertype].
+ bool isSubclassOf(Type type, Type supertype);
+
+ /// Tells whether [type] has a field or getter for [name].
+ bool hasGetter(Type type, Symbol name);
+
+ /// Tells whether [type] has a field or setter for [name].
+ bool hasSetter(Type type, Symbol name);
+
+ /// Tells whether [type] has a specific instance [method].
+ bool hasInstanceMethod(Type type, Symbol method);
+
+ /// Tells whether [type] has a specific static [method].
+ bool hasStaticMethod(Type type, Symbol method);
+
+ /// Get the declaration associated with field [name] in [type].
+ Declaration getDeclaration(Type type, Symbol name);
+
+ /// Retrieve all symbols of [type] that match [options].
+ List<Declaration> query(Type type, QueryOptions options);
+}
+
+/// A service that converts between [Symbol]s and [String]s.
+abstract class SymbolConverterService {
+ /// Returns the name associated with a [symbol].
+ String symbolToName(Symbol symbol);
+
+ /// Returns the symbol associated with a [name].
+ Symbol nameToSymbol(String name);
+}
diff --git a/pub/smoke/lib/src/common.dart b/pub/smoke/lib/src/common.dart
new file mode 100644
index 0000000..69cd8be
--- /dev/null
+++ b/pub/smoke/lib/src/common.dart
@@ -0,0 +1,210 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Some common utilities used by other libraries in this package.
+library smoke.src.common;
+
+import 'package:smoke/smoke.dart' as smoke show isSubclassOf;
+
+/// Returns [input] adjusted to be within [min] and [max] length. Truncating it
+/// if it's longer, or padding it with nulls if it's shorter. The returned list
+/// is a new copy if any modification is needed, otherwise [input] is returned.
+List adjustList(List input, int min, int max) {
+ if (input.length < min) {
+ return new List(min)..setRange(0, input.length, input);
+ }
+
+ if (input.length > max) {
+ return new List(max)..setRange(0, max, input);
+ }
+ return input;
+}
+
+/// Returns whether [metadata] contains any annotation that is either equal to
+/// an annotation in [queryAnnotations] or whose type is listed in
+/// [queryAnnotations].
+bool matchesAnnotation(Iterable metadata, Iterable queryAnnotations) {
+ for (var meta in metadata) {
+ for (var queryMeta in queryAnnotations) {
+ if (meta == queryMeta) return true;
+ if (queryMeta is Type && smoke.isSubclassOf(meta.runtimeType, queryMeta))
+ return true;
+ }
+ }
+ return false;
+}
+
+/// Number of arguments supported by [minArgs] and [maxArgs].
+const SUPPORTED_ARGS = 15;
+
+typedef _Func0();
+typedef _Func1(a);
+typedef _Func2(a, b);
+typedef _Func3(a, b, c);
+typedef _Func4(a, b, c, d);
+typedef _Func5(a, b, c, d, e);
+typedef _Func6(a, b, c, d, e, f);
+typedef _Func7(a, b, c, d, e, f, g);
+typedef _Func8(a, b, c, d, e, f, g, h);
+typedef _Func9(a, b, c, d, e, f, g, h, i);
+typedef _Func10(a, b, c, d, e, f, g, h, i, j);
+typedef _Func11(a, b, c, d, e, f, g, h, i, j, k);
+typedef _Func12(a, b, c, d, e, f, g, h, i, j, k, l);
+typedef _Func13(a, b, c, d, e, f, g, h, i, j, k, l, m);
+typedef _Func14(a, b, c, d, e, f, g, h, i, j, k, l, m, n);
+typedef _Func15(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o);
+
+/// Returns the minimum number of arguments that [f] takes as input, in other
+/// words, the total number of required arguments of [f]. If [f] expects more
+/// than [SUPPORTED_ARGS], this function returns `SUPPORTED_ARGS + 1`.
+///
+/// For instance, the current implementation only supports calculating the
+/// number of arguments between `0` and `3`. If the function takes `4` or more,
+/// this function automatically returns `4`.
+int minArgs(Function f) {
+ if (f is _Func0) return 0;
+ if (f is _Func1) return 1;
+ if (f is _Func2) return 2;
+ if (f is _Func3) return 3;
+ if (f is _Func4) return 4;
+ if (f is _Func5) return 5;
+ if (f is _Func6) return 6;
+ if (f is _Func7) return 7;
+ if (f is _Func8) return 8;
+ if (f is _Func9) return 9;
+ if (f is _Func10) return 10;
+ if (f is _Func11) return 11;
+ if (f is _Func12) return 12;
+ if (f is _Func13) return 13;
+ if (f is _Func14) return 14;
+ if (f is _Func15) return 15;
+ return SUPPORTED_ARGS + 1;
+}
+
+/// Returns the maximum number of arguments that [f] takes as input, which is
+/// the total number of required and optional arguments of [f]. If
+/// [f] may take more than [SUPPORTED_ARGS] required arguments, this function
+/// returns `-1`. However, if it takes less required arguments, but more than
+/// [SUPPORTED_ARGS] arguments including optional arguments, the result will be
+/// [SUPPORTED_ARGS].
+///
+/// For instance, the current implementation only supports calculating the
+/// number of arguments between `0` and [SUPPORTED_ARGS]. If the function
+/// takes more than [SUPPORTED_ARGS] mandatory arguments, this function
+/// returns `-1`, but if the funtion takes
+/// `8` mandatory arguments and `10` optional arguments, this function returns
+/// [SUPPORTED_ARGS].
+int maxArgs(Function f) {
+ // We could perform a full modified binary search but we really only care
+ // about performance for functions with fewer than 4 arguments.
+ if (f is! _Func2) {
+ if (f is _Func1) return 1;
+ if (f is _Func0) return 0;
+ if (f is! _Func4 && f is _Func3) return 3;
+ // Fall through to the slow case as the function has has maxArgs > 3.
+ } else if (f is! _Func4) {
+ return f is _Func3 ? 3 : 2;
+ }
+
+ if (f is _Func15) return 15;
+ if (f is _Func14) return 14;
+ if (f is _Func13) return 13;
+ if (f is _Func12) return 12;
+ if (f is _Func11) return 11;
+ if (f is _Func10) return 10;
+ if (f is _Func9) return 9;
+ if (f is _Func8) return 8;
+ if (f is _Func7) return 7;
+ if (f is _Func6) return 6;
+ if (f is _Func5) return 5;
+ if (f is _Func4) return 4;
+ if (f is _Func3) return 3;
+ if (f is _Func2) return 2;
+ if (f is _Func1) return 1;
+ if (f is _Func0) return 0;
+ return -1;
+}
+
+/// Returns whether [f] can accept [n] arguments.
+/// This is equivalent to
+/// `n >= minArgs(f) && n <= maxArgs(f)`
+/// when [f] accepts at most [SUPPORTED_ARGS].
+bool canAcceptNArgs(Function f, int n) {
+ switch (n) {
+ case 0:
+ return f is _Func0;
+ case 1:
+ return f is _Func1;
+ case 2:
+ return f is _Func2;
+ case 3:
+ return f is _Func3;
+ case 4:
+ return f is _Func4;
+ case 5:
+ return f is _Func5;
+ case 6:
+ return f is _Func6;
+ case 7:
+ return f is _Func7;
+ case 8:
+ return f is _Func8;
+ case 9:
+ return f is _Func9;
+ case 10:
+ return f is _Func10;
+ case 11:
+ return f is _Func11;
+ case 12:
+ return f is _Func12;
+ case 13:
+ return f is _Func13;
+ case 14:
+ return f is _Func14;
+ case 15:
+ return f is _Func15;
+ }
+ return false;
+}
+
+/// Shallow comparison of two lists.
+bool compareLists(List a, List b, {bool unordered: false}) {
+ if (a == null && b != null) return false;
+ if (a != null && b == null) return false;
+ if (a.length != b.length) return false;
+ if (unordered) {
+ var countMap = {};
+ for (var x in b) {
+ var count = countMap[x];
+ if (count == null) count = 0;
+ countMap[x] = count + 1;
+ }
+ for (var x in a) {
+ var count = countMap[x];
+ if (count == null) return false;
+ if (count == 1) {
+ countMap.remove(x);
+ } else {
+ countMap[x] = count - 1;
+ }
+ }
+ return countMap.isEmpty;
+ } else {
+ for (int i = 0; i < a.length; i++) {
+ if (a[i] != b[i]) return false;
+ }
+ }
+ return true;
+}
+
+/// Shallow comparison of two maps.
+bool compareMaps(Map a, Map b) {
+ if (a == null && b != null) return false;
+ if (a != null && b == null) return false;
+ if (a.length != b.length) return false;
+ for (var k in a.keys) {
+ if (!b.containsKey(k) || a[k] != b[k]) return false;
+ }
+ return true;
+}
diff --git a/pub/smoke/lib/src/default_transformer.dart b/pub/smoke/lib/src/default_transformer.dart
new file mode 100644
index 0000000..c5e93cc
--- /dev/null
+++ b/pub/smoke/lib/src/default_transformer.dart
@@ -0,0 +1,47 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Transformer that replaces the default mirror-based implementation of smoke,
+/// so that during deploy smoke doesn't include any dependency on dart:mirrors.
+library smoke.src.default_transformer;
+
+import 'dart:async';
+import 'package:barback/barback.dart';
+
+/// Replaces the default mirror-based implementation of smoke in
+/// `pacakge:smoke/implementation.dart`, so that during deploy smoke doesn't
+/// include any dependency on dart:mirrors.
+// TODO(sigmund): include tests that run this transformation automatically.
+class DefaultTransformer extends Transformer {
+ DefaultTransformer.asPlugin();
+
+ /// Only apply to `lib/src/implementation.dart`.
+ // TODO(nweiz): This should just take an AssetId when barback <0.13.0 support
+ // is dropped.
+ Future<bool> isPrimary(idOrAsset) {
+ assert(idOrAsset is Asset || idOrAsset is AssetId);
+ var id = idOrAsset is AssetId ? idOrAsset : (idOrAsset as Asset).id;
+ return new Future.value(
+ id.package == 'smoke' && id.path == 'lib/src/implementation.dart');
+ }
+
+ Future apply(Transform transform) {
+ var id = transform.primaryInput.id;
+ return transform.primaryInput.readAsString().then((code) {
+ // Note: this rewrite is highly-coupled with how implementation.dart is
+ // written. Make sure both are updated in sync.
+ transform.addOutput(new Asset.fromString(
+ id,
+ code
+ .replaceAll(new RegExp('new Reflective[^;]*;'),
+ 'throwNotConfiguredError();')
+ .replaceAll("import 'package:smoke/mirrors.dart';", '')));
+ });
+ }
+}
+
+/** Transformer phases which should be applied to the smoke package. */
+List<List<Transformer>> get phasesForSmoke => [
+ [new DefaultTransformer.asPlugin()]
+ ];
diff --git a/pub/smoke/lib/src/implementation.dart b/pub/smoke/lib/src/implementation.dart
new file mode 100644
index 0000000..51d8b9c
--- /dev/null
+++ b/pub/smoke/lib/src/implementation.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A library that is used to select the default implementation of smoke. During
+/// development we use a default mirror-based implementation, for deployment we
+/// let the main program set programatically what implementation to use (likely
+/// one based on static code generation).
+library smoke.src.implementation;
+
+// IMPORTANT NOTE: This file is edited by a transformer in this package
+// (default_transformer.dart), so any edits here should be coordinated with
+// changes there.
+
+import 'package:smoke/mirrors.dart';
+import 'package:smoke/smoke.dart';
+
+/// Implementation of [ObjectAccessorService] in use, initialized lazily so it
+/// can be replaced at deployment time with an efficient alternative.
+ObjectAccessorService objectAccessor = new ReflectiveObjectAccessorService();
+
+/// Implementation of [TypeInspectorService] in use, initialized lazily so it
+/// can be replaced at deployment time with an efficient alternative.
+TypeInspectorService typeInspector = new ReflectiveTypeInspectorService();
+
+/// Implementation of [SymbolConverterService] in use, initialized lazily so it
+/// can be replaced at deployment time with an efficient alternative.
+SymbolConverterService symbolConverter = new ReflectiveSymbolConverterService();
+
+throwNotConfiguredError() {
+ throw new Exception('The "smoke" library has not been configured. '
+ 'Make sure you import and configure one of the implementations ('
+ 'package:smoke/mirrors.dart or package:smoke/static.dart).');
+}
diff --git a/pub/smoke/lib/static.dart b/pub/smoke/lib/static.dart
new file mode 100644
index 0000000..40ae158
--- /dev/null
+++ b/pub/smoke/lib/static.dart
@@ -0,0 +1,297 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Static implementation of smoke services using code-generated data.
+library smoke.static;
+
+import 'dart:math' as math;
+
+import 'package:smoke/smoke.dart';
+
+import 'src/common.dart';
+
+typedef T Getter<T>(object);
+typedef void Setter<T>(object, value);
+
+class StaticConfiguration {
+ /// Maps symbol to a function that reads that symbol of an object. For
+ /// instance, `#i: (o) => o.i`.
+ final Map<Symbol, Getter> getters;
+
+ /// Maps symbol to a function that updates that symbol of an object. For
+ /// instance, `#i: (o, v) { o.i = v; }`.
+ final Map<Symbol, Setter> setters;
+
+ /// Maps a type to its super class. For example, String: Object.
+ final Map<Type, Type> parents;
+
+ /// For each type, a map of declarations per symbol (property or method).
+ final Map<Type, Map<Symbol, Declaration>> declarations;
+
+ /// Static methods for each type.
+ // TODO(sigmund): should we add static getters & setters too?
+ final Map<Type, Map<Symbol, Function>> staticMethods;
+
+ /// A map from symbol to strings.
+ final Map<Symbol, String> names;
+
+ /// A map from strings to symbols (the reverse of [names]).
+ final Map<String, Symbol> _symbols = {};
+
+ /// Whether to check for missing declarations, otherwise, return default
+ /// values (for example a missing parent class can be treated as Object)
+ final bool checkedMode;
+
+ StaticConfiguration(
+ {Map<Symbol, Getter> getters,
+ Map<Symbol, Setter> setters,
+ Map<Type, Type> parents,
+ Map<Type, Map<Symbol, Declaration>> declarations,
+ Map<Type, Map<Symbol, Function>> staticMethods,
+ Map<Symbol, String> names,
+ this.checkedMode: true})
+ : getters = getters != null ? getters : {},
+ setters = setters != null ? setters : {},
+ parents = parents != null ? parents : {},
+ declarations = declarations != null ? declarations : {},
+ staticMethods = staticMethods != null ? staticMethods : {},
+ names = names != null ? names : {} {
+ this.names.forEach((k, v) {
+ _symbols[v] = k;
+ });
+ }
+
+ void addAll(StaticConfiguration other) {
+ getters.addAll(other.getters);
+ setters.addAll(other.setters);
+ parents.addAll(other.parents);
+ _nestedAddAll(declarations, other.declarations);
+ _nestedAddAll(staticMethods, other.staticMethods);
+ names.addAll(other.names);
+ other.names.forEach((k, v) {
+ _symbols[v] = k;
+ });
+ }
+
+ static _nestedAddAll(Map a, Map b) {
+ for (var key in b.keys) {
+ a.putIfAbsent(key, () => {});
+ a[key].addAll(b[key]);
+ }
+ }
+}
+
+/// Set up the smoke package to use a static implementation based on the given
+/// [configuration].
+useGeneratedCode(StaticConfiguration configuration) {
+ configure(
+ new GeneratedObjectAccessorService(configuration),
+ new GeneratedTypeInspectorService(configuration),
+ new GeneratedSymbolConverterService(configuration));
+}
+
+/// Implements [ObjectAccessorService] using a static configuration.
+class GeneratedObjectAccessorService implements ObjectAccessorService {
+ final StaticConfiguration _configuration;
+ Map<Symbol, Getter> get _getters => _configuration.getters;
+ Map<Symbol, Setter> get _setters => _configuration.setters;
+ Map<Type, Map<Symbol, Function>> get _staticMethods =>
+ _configuration.staticMethods;
+
+ GeneratedObjectAccessorService(this._configuration);
+
+ read(Object object, Symbol name) {
+ var getter = _getters[name];
+ if (getter == null) {
+ throw new MissingCodeException('getter "$name" in $object');
+ }
+ return getter(object);
+ }
+
+ void write(Object object, Symbol name, value) {
+ var setter = _setters[name];
+ if (setter == null) {
+ throw new MissingCodeException('setter "$name" in $object');
+ }
+ setter(object, value);
+ }
+
+ invoke(object, Symbol name, List args, {Map namedArgs, bool adjust: false}) {
+ var method;
+ if (object is Type && name != #toString) {
+ var classMethods = _staticMethods[object];
+ method = classMethods == null ? null : classMethods[name];
+ } else {
+ var getter = _getters[name];
+ method = getter == null ? null : getter(object);
+ }
+ if (method == null) {
+ throw new MissingCodeException('method "$name" in $object');
+ }
+ var tentativeError;
+ if (adjust) {
+ var min = minArgs(method);
+ if (min > SUPPORTED_ARGS) {
+ tentativeError = 'we tried to adjust the arguments for calling "$name"'
+ ', but we couldn\'t determine the exact number of arguments it '
+ 'expects (it is more than $SUPPORTED_ARGS).';
+ // The argument list might be correct, so we still invoke the function
+ // and let the user see the error.
+ args = adjustList(args, min, math.max(min, args.length));
+ } else {
+ var max = maxArgs(method);
+ args = adjustList(args, min, max >= 0 ? max : args.length);
+ }
+ }
+ if (namedArgs != null) {
+ throw new UnsupportedError(
+ 'smoke.static doesn\'t support namedArguments in invoke');
+ }
+ try {
+ return Function.apply(method, args);
+ } on NoSuchMethodError catch (_) {
+ // TODO(sigmund): consider whether this should just be in a logger or if
+ // we should wrap `e` as a new exception (what's the best way to let users
+ // know about this tentativeError?)
+ if (tentativeError != null) print(tentativeError);
+ rethrow;
+ }
+ }
+}
+
+/// Implements [TypeInspectorService] using a static configuration.
+class GeneratedTypeInspectorService implements TypeInspectorService {
+ final StaticConfiguration _configuration;
+
+ Map<Type, Type> get _parents => _configuration.parents;
+ Map<Type, Map<Symbol, Declaration>> get _declarations =>
+ _configuration.declarations;
+ bool get _checkedMode => _configuration.checkedMode;
+
+ GeneratedTypeInspectorService(this._configuration);
+
+ bool isSubclassOf(Type type, Type supertype) {
+ if (type == supertype || supertype == Object) return true;
+ while (type != Object) {
+ var parentType = _parents[type];
+ if (parentType == supertype) return true;
+ if (parentType == null) {
+ if (!_checkedMode) return false;
+ throw new MissingCodeException('superclass of "$type" ($parentType)');
+ }
+ type = parentType;
+ }
+ return false;
+ }
+
+ bool hasGetter(Type type, Symbol name) {
+ var decl = _findDeclaration(type, name);
+ // No need to check decl.isProperty because methods are also automatically
+ // considered getters (auto-closures).
+ return decl != null && !decl.isStatic;
+ }
+
+ bool hasSetter(Type type, Symbol name) {
+ var decl = _findDeclaration(type, name);
+ return decl != null && !decl.isMethod && !decl.isFinal && !decl.isStatic;
+ }
+
+ bool hasInstanceMethod(Type type, Symbol name) {
+ var decl = _findDeclaration(type, name);
+ return decl != null && decl.isMethod && !decl.isStatic;
+ }
+
+ bool hasStaticMethod(Type type, Symbol name) {
+ final map = _declarations[type];
+ if (map == null) {
+ if (!_checkedMode) return false;
+ throw new MissingCodeException('declarations for $type');
+ }
+ final decl = map[name];
+ return decl != null && decl.isMethod && decl.isStatic;
+ }
+
+ Declaration getDeclaration(Type type, Symbol name) {
+ var decl = _findDeclaration(type, name);
+ if (decl == null) {
+ if (!_checkedMode) return null;
+ throw new MissingCodeException('declaration for $type.$name');
+ }
+ return decl;
+ }
+
+ List<Declaration> query(Type type, QueryOptions options) {
+ var result = <Declaration>[];
+ if (options.includeInherited) {
+ var superclass = _parents[type];
+ if (superclass == null) {
+ if (_checkedMode) {
+ throw new MissingCodeException('superclass of "$type"');
+ }
+ } else if (superclass != options.includeUpTo) {
+ result = query(superclass, options);
+ }
+ }
+ var map = _declarations[type];
+ if (map == null) {
+ if (!_checkedMode) return result;
+ throw new MissingCodeException('declarations for $type');
+ }
+ for (var decl in map.values) {
+ if (!options.includeFields && decl.isField) continue;
+ if (!options.includeProperties && decl.isProperty) continue;
+ if (options.excludeFinal && decl.isFinal) continue;
+ if (!options.includeMethods && decl.isMethod) continue;
+ if (options.matches != null && !options.matches(decl.name)) continue;
+ if (options.withAnnotations != null &&
+ !matchesAnnotation(decl.annotations, options.withAnnotations)) {
+ continue;
+ }
+ if (options.excludeOverriden) {
+ result.retainWhere((value) => decl.name != value.name);
+ }
+ result.add(decl);
+ }
+ return result;
+ }
+
+ Declaration _findDeclaration(Type type, Symbol name) {
+ while (type != Object) {
+ final declarations = _declarations[type];
+ if (declarations != null) {
+ final declaration = declarations[name];
+ if (declaration != null) return declaration;
+ }
+ var parentType = _parents[type];
+ if (parentType == null) {
+ if (!_checkedMode) return null;
+ throw new MissingCodeException('superclass of "$type"');
+ }
+ type = parentType;
+ }
+ return null;
+ }
+}
+
+/// Implements [SymbolConverterService] using a static configuration.
+class GeneratedSymbolConverterService implements SymbolConverterService {
+ final StaticConfiguration _configuration;
+ Map<Symbol, String> get _names => _configuration.names;
+ Map<String, Symbol> get _symbols => _configuration._symbols;
+
+ GeneratedSymbolConverterService(this._configuration);
+
+ String symbolToName(Symbol symbol) => _names[symbol];
+ Symbol nameToSymbol(String name) => _symbols[name];
+}
+
+/// Exception thrown when trynig to access something that should be there, but
+/// the code generator didn't include it.
+class MissingCodeException implements Exception {
+ final String description;
+ MissingCodeException(this.description);
+
+ String toString() => 'Missing $description. '
+ 'Code generation for the smoke package seems incomplete.';
+}
diff --git a/pub/smoke/lib/static_debug.dart b/pub/smoke/lib/static_debug.dart
new file mode 100644
index 0000000..4ebca72
--- /dev/null
+++ b/pub/smoke/lib/static_debug.dart
@@ -0,0 +1,120 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Static implementation of smoke services that uses code-generated data and
+/// verifies that the results match what we would get with a mirror-based
+/// implementation.
+library smoke.static_debug;
+
+export 'package:smoke/static.dart' show StaticConfiguration, Getter, Setter;
+import 'package:smoke/static.dart';
+import 'package:smoke/mirrors.dart';
+import 'package:smoke/smoke.dart';
+
+import 'src/common.dart' show compareLists;
+
+/// Set up the smoke package to use a static implementation based on the given
+/// [configuration].
+useGeneratedCode(StaticConfiguration configuration) {
+ configure(
+ new _DebugObjectAccessorService(configuration),
+ new _DebugTypeInspectorService(configuration),
+ new _DebugSymbolConverterService(configuration));
+}
+
+/// Implements [ObjectAccessorService] using a static configuration.
+class _DebugObjectAccessorService implements ObjectAccessorService {
+ GeneratedObjectAccessorService _static;
+ ReflectiveObjectAccessorService _mirrors;
+
+ _DebugObjectAccessorService(StaticConfiguration configuration)
+ : _static = new GeneratedObjectAccessorService(configuration),
+ _mirrors = new ReflectiveObjectAccessorService();
+
+ read(Object object, Symbol name) => _check('read', [object, name],
+ _static.read(object, name), _mirrors.read(object, name));
+
+ // Note: we can't verify operations with side-effects like write or invoke.
+ void write(Object object, Symbol name, value) =>
+ _static.write(object, name, value);
+
+ invoke(object, Symbol name, List args, {Map namedArgs, bool adjust: false}) =>
+ _static.invoke(object, name, args, namedArgs: namedArgs, adjust: adjust);
+}
+
+/// Implements [TypeInspectorService] using a static configuration.
+class _DebugTypeInspectorService implements TypeInspectorService {
+ GeneratedTypeInspectorService _static;
+ ReflectiveTypeInspectorService _mirrors;
+
+ _DebugTypeInspectorService(StaticConfiguration configuration)
+ : _static = new GeneratedTypeInspectorService(configuration),
+ _mirrors = new ReflectiveTypeInspectorService();
+
+ bool isSubclassOf(Type type, Type supertype) => _check(
+ 'isSubclassOf',
+ [type, supertype],
+ _static.isSubclassOf(type, supertype),
+ _mirrors.isSubclassOf(type, supertype));
+
+ bool hasGetter(Type type, Symbol name) => _check('hasGetter', [type, name],
+ _static.hasGetter(type, name), _mirrors.hasGetter(type, name));
+
+ bool hasSetter(Type type, Symbol name) => _check('hasSetter', [type, name],
+ _static.hasSetter(type, name), _mirrors.hasSetter(type, name));
+
+ bool hasInstanceMethod(Type type, Symbol name) => _check(
+ 'hasInstanceMethod',
+ [type, name],
+ _static.hasInstanceMethod(type, name),
+ _mirrors.hasInstanceMethod(type, name));
+
+ bool hasStaticMethod(Type type, Symbol name) => _check(
+ 'hasStaticMethod',
+ [type, name],
+ _static.hasStaticMethod(type, name),
+ _mirrors.hasStaticMethod(type, name));
+
+ Declaration getDeclaration(Type type, Symbol name) => _check(
+ 'getDeclaration',
+ [type, name],
+ _static.getDeclaration(type, name),
+ _mirrors.getDeclaration(type, name));
+
+ List<Declaration> query(Type type, QueryOptions options) => _check(
+ 'query',
+ [type, options],
+ _static.query(type, options),
+ _mirrors.query(type, options));
+}
+
+/// Implements [SymbolConverterService] using a static configuration.
+class _DebugSymbolConverterService implements SymbolConverterService {
+ GeneratedSymbolConverterService _static;
+ ReflectiveSymbolConverterService _mirrors;
+
+ _DebugSymbolConverterService(StaticConfiguration configuration)
+ : _static = new GeneratedSymbolConverterService(configuration),
+ _mirrors = new ReflectiveSymbolConverterService();
+
+ String symbolToName(Symbol symbol) => _check('symbolToName', [symbol],
+ _static.symbolToName(symbol), _mirrors.symbolToName(symbol));
+
+ Symbol nameToSymbol(String name) => _check('nameToSymbol', [name],
+ _static.nameToSymbol(name), _mirrors.nameToSymbol(name));
+}
+
+dynamic/*=X*/ _check/*<X>*/(String operation, List arguments,
+ dynamic/*=X*/ staticResult, mirrorResult) {
+ if (staticResult == mirrorResult) return staticResult;
+ if (staticResult is List &&
+ mirrorResult is List &&
+ compareLists(staticResult as List, mirrorResult, unordered: true)) {
+ return staticResult;
+ }
+ print('warning: inconsistent result on $operation(${arguments.join(', ')})\n'
+ 'smoke.mirrors result: $mirrorResult\n'
+ 'smoke.static result: $staticResult\n');
+ return staticResult;
+}
diff --git a/pub/smoke/pubspec.yaml b/pub/smoke/pubspec.yaml
new file mode 100644
index 0000000..7e9777b
--- /dev/null
+++ b/pub/smoke/pubspec.yaml
@@ -0,0 +1,24 @@
+name: smoke
+version: 0.3.6+2
+author: Polymer.dart Authors <web-ui-dev@dartlang.org>
+homepage: https://github.com/dart-lang/smoke
+description: >
+ A restricted reflective system that uses mirrors at development time, but that
+ can be replaced with non-reflective calls using code generation. See README.md
+ for mode details.
+dependencies:
+ barback: ">=0.9.0 <0.16.0"
+ logging: ">=0.9.0 <0.12.0"
+ analyzer: "^0.27.0"
+# TODO(sigmund): once we have some easier way to do global app-level
+# transformers, we might want to remove this transformer here and only apply it
+# in apps that need it.
+transformers:
+- smoke/src/default_transformer:
+ $include: lib/src/implementation.dart
+dev_dependencies:
+ test: "^0.12.0"
+ path: "^1.0.0"
+ transformer_test: "^0.2.0"
+environment:
+ sdk: ">=1.12.0 <2.0.0"